--------------Spare Change-------------
A 4am & san inc crack        2023-08-23
---------------------------------------

Name: Spare Change
Genre: arcade
Year: 1983
Credits: Dan & Mike Zeller
Publisher: Broderbund Software
Platform: Apple ][+ or later (48K)
Media: single-sided 5.25-inch floppy
OS: custom
Other versions:
  Apple Bandit / MPG
  Aldo Reset / CCB

                   ~

               Chapter 0
 In Which Various Automated Tools Fail
          In Interesting Ways


COPYA
  immediate disk read error

Locksmith Fast Disk Backup
  unable to read any track

EDD 4 bit copy (no sync, no count)
  Backup copy never gets off track 0

Copy ][+ nibble editor
  T00 has a modified address prologue
    (D5 AA B5) and modified epilogues
  T01+ appears to be 4-4 encoded data
    (2 nibbles on disk = 1 byte in
    memory) with a custom prologue/
    delimiter. In any case, it's
    neither 13 nor 16 sectors.

Disk Fixer
  not much help

Why didn't COPYA work?
  not a 16-sector disk

Why didn't Locksmith FDB work?
  ditto

Why didn't my EDD copy work?
  I don't know. Early Broderbund games
  loved using structural protection,
  like storing data on half tracks.
  This was matched only by their love
  of runtime protection checks.

This is decidedly not a single-load
game. There is disk access before and
after each level, and before each
intermission cartoon.

Combined with the early indications of
a custom bootloader and 4-4 encoded
sectors, this is not going to be a
straightforward crack by any definition
of "straight" or "forward."

Let's start at the beginning.

                   ~

               Chapter 1
      In Which We Brag About Our
           Humble Beginnings


I have two floppy drives, one in slot 6
and the other in slot 5. My "work disk"
(in slot 5) runs Diversi-DOS 64K, which
is compatible with Apple DOS 3.3 but
relocates most of DOS to the language
card on boot. This frees up most of
main memory (only using a single page
at $BF00..$BFFF), which is useful for
loading large files or examining code
that lives in areas typically reserved
for DOS.

[S6,D1=original disk]
[S5,D1=my work disk]

The floppy drive firmware code at $C600
is responsible for aligning the drive
head and reading sector 0 of track 0
into main memory at $0800. Because the
drive can be connected to any slot, the
firmware code can't assume it's loaded
at $C600. If the floppy drive card were
removed from slot 6 and reinstalled in
slot 5, the firmware code would load at
$C500 instead.

To accommodate this, the firmware does
some fancy stack manipulation to detect
where it is in memory (which is a neat
trick, since the 6502 program counter
is not generally accessible). However,
due to space constraints, the detection
code only cares about the lower 4 bits
of the high byte of its own address.

Stay with me, this is all about to come
together and go boom.

$C600 (or $C500, or anywhere in $Cx00)
is read-only memory. I can't change it,
which means I can't stop it from
transferring control to the boot sector
of the disk once it's in memory. BUT!
The disk firmware code works unmodified
at any address. Any address that ends
with $x600 will boot slot 6, including
$B600, $A600, $9600, &c.

; copy drive firmware to $9600
*9600<C600.C6FFM

; and execute it
*9600G
...reboots slot 6, loads game...

Now then:

]PR#5
...
]CALL -151

*9600<C600.C6FFM

*96F8L

96F8-   4C 01 08    JMP   $0801

That's where the disk controller ROM
code ends and the on-disk code begins.
But $9600 is part of read/write memory.
I can change it at will. So I can
interrupt the boot process after the
drive firmware loads the boot sector
from the disk but before it transfers
control to the disk's bootloader.

; instead of jumping to on-disk code,
; copy boot sector to higher memory so
; it survives a reboot
96F8-   A0 00       LDY   #$00
96FA-   B9 00 08    LDA   $0800,Y
96FD-   99 00 28    STA   $2800,Y
9700-   C8          INY
9701-   D0 F7       BNE   $96FA

; turn off slot 6 drive motor
9703-   AD E8 C0    LDA   $C0E8

; reboot to my work disk in slot 5
9706-   4C 00 C5    JMP   $C500

*BSAVE TRACE,A$9600,L$109
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE BOOT.0800-08FF,A$2800,L$100

Now we get to(*) trace the boot process
one sector, one page, one instruction
at a time.

(*) If you replace the words "need to"
    with the words "get to," life
    becomes amazing.

                   ~

               Chapter 2
         In Which We Discover
        All Manner of Mischief


]CALL -151

; copy code back to $0800 where it was
; originally loaded, to make it easier
; to follow
*800<2800.28FFM

*801L

; immediately move this code to the
; input buffer at $0200
0801-   A2 00       LDX   #$00
0803-   BD 00 08    LDA   $0800,X
0806-   9D 00 02    STA   $0200,X
0809-   E8          INX
080A-   D0 F7       BNE   $0803
080C-   4C 0F 02    JMP   $020F

OK, I can do that too. Well, mostly.
The page at $0200 is the text input
buffer, used by both Applesoft BASIC
and the built-in monitor (which I'm in
right now). But I can copy enough of it
to examine this code in situ.

*20F<80F.8FFM

*20FL

; set up a nibble translation table at
; $0800
020F-   A0 AB       LDY   #$AB
0211-   98          TYA
0212-   85 3C       STA   $3C
0214-   4A          LSR
0215-   05 3C       ORA   $3C
0217-   C9 FF       CMP   #$FF
0219-   D0 09       BNE   $0224
021B-   C0 D5       CPY   #$D5
021D-   F0 05       BEQ   $0224
021F-   8A          TXA
0220-   99 00 08    STA   $0800,Y
0223-   E8          INX
0224-   C8          INY
0225-   D0 EA       BNE   $0211
0227-   84 3D       STY   $3D

; #$00 into zero page $26 and #$03 into
; $27 means we're probably going to be
; loading data into $0300..$03FF later,
; because ($26) points to $0300.
0229-   84 26       STY   $26
022B-   A9 03       LDA   #$03
022D-   85 27       STA   $27

; zero page $2B holds the boot slot x16
022F-   A6 2B       LDX   $2B
0231-   20 5D 02    JSR   $025D

*25DL

; read a sector from track $00 (this is
; actually derived from the code in the
; disk controller ROM routine at $C65C,
; but looking for an address prologue
; of "D5 AA B5" instead of "D5 AA 96")
; and using the nibble translation
; table we set up earlier at $0800
025D-   18          CLC
025E-   08          PHP
025F-   BD 8C C0    LDA   $C08C,X
0262-   10 FB       BPL   $025F
0264-   49 D5       EOR   #$D5
0266-   D0 F7       BNE   $025F
0268-   BD 8C C0    LDA   $C08C,X
026B-   10 FB       BPL   $0268
026D-   C9 AA       CMP   #$AA
026F-   D0 F3       BNE   $0264
0271-   EA          NOP
0272-   BD 8C C0    LDA   $C08C,X
0275-   10 FB       BPL   $0272

; #$B5 for third prologue nibble
0277-   C9 B5       CMP   #$B5
0279-   F0 09       BEQ   $0284
027B-   28          PLP
027C-   90 DF       BCC   $025D
027E-   49 AD       EOR   #$AD
0280-   F0 1F       BEQ   $02A1
0282-   D0 D9       BNE   $025D
0284-   A0 03       LDY   #$03
0286-   84 2A       STY   $2A
0288-   BD 8C C0    LDA   $C08C,X
028B-   10 FB       BPL   $0288
028D-   2A          ROL
028E-   85 3C       STA   $3C
0290-   BD 8C C0    LDA   $C08C,X
0293-   10 FB       BPL   $0290
0295-   25 3C       AND   $3C
0297-   88          DEY
0298-   D0 EE       BNE   $0288
029A-   28          PLP
029B-   C5 3D       CMP   $3D
029D-   D0 BE       BNE   $025D
029F-   B0 BD       BCS   $025E
02A1-   A0 9A       LDY   #$9A
02A3-   84 3C       STY   $3C
02A5-   BC 8C C0    LDY   $C08C,X
02A8-   10 FB       BPL   $02A5

; use the nibble translation table we
; set up earlier to convert nibbles on
; disk into bytes in memory
02AA-   59 00 08    EOR   $0800,Y
02AD-   A4 3C       LDY   $3C
02AF-   88          DEY
02B0-   99 00 08    STA   $0800,Y
02B3-   D0 EE       BNE   $02A3
02B5-   84 3C       STY   $3C
02B7-   BC 8C C0    LDY   $C08C,X
02BA-   10 FB       BPL   $02B7
02BC-   59 00 08    EOR   $0800,Y
02BF-   A4 3C       LDY   $3C

; store the converted bytes at $0300
02C1-   91 26       STA   ($26),Y
02C3-   C8          INY
02C4-   D0 EF       BNE   $02B5

; verify the data with a one-nibble
; checksum
02C6-   BC 8C C0    LDY   $C08C,X
02C9-   10 FB       BPL   $02C6
02CB-   59 00 08    EOR   $0800,Y
02CE-   D0 8D       BNE   $025D
02D0-   60          RTS

Continuing from $0234...

*234L

0234-   20 D1 02    JSR   $02D1

*2D1L

; finish decoding nibbles
02D1-   A8          TAY
02D2-   A2 00       LDX   #$00
02D4-   B9 00 08    LDA   $0800,Y
02D7-   4A          LSR
02D8-   3E CC 03    ROL   $03CC,X
02DB-   4A          LSR
02DC-   3E 99 03    ROL   $0399,X
02DF-   85 3C       STA   $3C
02E1-   B1 26       LDA   ($26),Y
02E3-   0A          ASL
02E4-   0A          ASL
02E5-   0A          ASL
02E6-   05 3C       ORA   $3C
02E8-   91 26       STA   ($26),Y
02EA-   C8          INY
02EB-   E8          INX
02EC-   E0 33       CPX   #$33
02EE-   D0 E4       BNE   $02D4
02F0-   C6 2A       DEC   $2A
02F2-   D0 DE       BNE   $02D2

; verify final checksum
02F4-   CC 00 03    CPY   $0300
02F7-   D0 03       BNE   $02FC

; checksum passed, return to caller and
; continue with the boot process
02F9-   60          RTS

; checksum failed, print "ERR" and exit
02FC-   4C 2D FF    JMP   $FF2D

Continuing from $0237...

*237L

; jump into the code we just read
0237-   4C 01 03    JMP   $0301

This is where I get to interrupt the
boot, before it jumps to $0301.

                   ~

               Chapter 3
         In Which We Get Lost
        In A Forest of Mirrors


*9600<C600.C6FFM

; patch boot sector so it calls my
; routine instead of jumping to $0301
96F8-   A9 05       LDA   #$05
96FA-   8D 38 08    STA   $0838
96FD-   A9 97       LDA   #$97
96FF-   8D 39 08    STA   $0839

; start the boot
9702-   4C 01 08    JMP   $0801

; (callback is here) copy the code at
; $0300 to higher memory so it survives
; a reboot
9705-   A0 00       LDY   #$00
9707-   B9 00 03    LDA   $0300,Y
970A-   99 00 23    STA   $2300,Y
970D-   C8          INY
970E-   D0 F7       BNE   $9707

; turn off slot 6 drive motor and
; reboot to my work disk in slot 5
9710-   AD E8 C0    LDA   $C0E8
9713-   4C 00 C5    JMP   $C500

*BSAVE TRACE2,A$9600,L$116
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE BOOT.0300-03FF,A$2300,L$100
]CALL -151

*2301L

2301-   84 48       STY   $48

; clear hi-res graphics screen 2
2303-   A0 00       LDY   #$00
2305-   98          TYA
2306-   A2 20       LDX   #$20
2308-   99 00 40    STA   $4000,Y
230B-   C8          INY
230C-   D0 FA       BNE   $2308
230E-   EE 0A 03    INC   $030A
2311-   CA          DEX
2312-   D0 F4       BNE   $2308

; and show it (appears blank)
2314-   AD 57 C0    LDA   $C057
2317-   AD 52 C0    LDA   $C052
231A-   AD 55 C0    LDA   $C055
231D-   AD 50 C0    LDA   $C050

; decrypt the rest of this page to the
; stack page at $0100
2320-   B9 00 03    LDA   $0300,Y
2323-   45 48       EOR   $48
2325-   99 00 01    STA   $0100,Y
2328-   C8          INY
2329-   D0 F5       BNE   $2320

; set the stack pointer
232B-   A2 CF       LDX   #$CF
232D-   9A          TXS

; and exit via RTS
232E-   60          RTS

Oh joy, stack manipulation. The stack
on an Apple II is just $100 bytes in
main memory ($0100..$01FF) and a single
byte register that serves as an index
into that page. This allows for all
manner of mischief -- overwriting the
stack page (as we're doing here),
manually changing the stack pointer
(also doing that here), or even putting
executable code directly on the stack.

The upshot is that I have no idea where
execution continues next, because I
don't know what ends up on the stack
page. I get to interrupt the boot again
to see the decrypted data that ends up
at $0100.

                   ~

               Chapter 4
    In Which Everything Is Off By 1


*BLOAD TRACE2

[first part is the same as the
 previous trace]

; reproduce the decryption loop, but
; store the result at $2100 so it
; survives a reboot
9705-   84 48       STY   $48
9707-   A0 00       LDY   #$00
9709-   B9 00 03    LDA   $0300,Y
970C-   45 48       EOR   $48
970E-   99 00 21    STA   $2100,Y
9711-   C8          INY
9712-   D0 F5       BNE   $9709

; turn off drive motor and reboot to
; my work disk
9714-   AD E8 C0    LDA   $C0E8
9717-   4C 00 C5    JMP   $C500

*BSAVE TRACE3,A$9600,L$11A
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE BOOT.0100-01FF,A$2100,L$100
]CALL -151

The original code at $0300 manually
reset the stack pointer to #$CF and
exited via RTS. The Apple II will
increment the stack pointer before
using it as an index into $0100 to get
the next address. (For reasons I won't
get into here, it also increments the
address before passing execution to
it.)

*21D0.

21D0- 2F 01 FF 03 FF 04 4F 04
      ^^^^^
 next return address

$012F + 1 = $0130, which is already in
memory at $2130.

Oh joy. Code on the stack. (Remember,
the "stack" is just a page in main
memory. If you want to use that page
for something else, it's up to you to
ensure that it doesn't conflict with
the stack functioning as a stack.)

*2130L

2130-   A2 04       LDX   #$04
2132-   86 86       STX   $86
2134-   A0 00       LDY   #$00
2136-   84 83       STY   $83
2138-   86 84       STX   $84

Now ($83) points to $0400.

; get slot number (x16)
213A-   A6 2B       LDX   $2B

; find a 3-nibble prologue ("BF D7 D5")
213C-   BD 8C C0    LDA   $C08C,X
213F-   10 FB       BPL   $213C
2141-   C9 BF       CMP   #$BF
2143-   D0 F7       BNE   $213C
2145-   BD 8C C0    LDA   $C08C,X
2148-   10 FB       BPL   $2145
214A-   C9 D7       CMP   #$D7
214C-   D0 F3       BNE   $2141
214E-   BD 8C C0    LDA   $C08C,X
2151-   10 FB       BPL   $214E
2153-   C9 D5       CMP   #$D5
2155-   D0 F3       BNE   $214A

; read 4-4-encoded data
2157-   BD 8C C0    LDA   $C08C,X
215A-   10 FB       BPL   $2157
215C-   2A          ROL
215D-   85 85       STA   $85
215F-   BD 8C C0    LDA   $C08C,X
2162-   10 FB       BPL   $215F
2164-   25 85       AND   $85

; store in $0400 (text page, but it's
; hidden right now because we switched
; to hi-res graphics screen 2 at $0314)
2166-   91 83       STA   ($83),Y
2168-   C8          INY
2169-   D0 EC       BNE   $2157

; find a 1-nibble epilogue ("D4")
216B-   0E 00 C0    ASL   $C000
216E-   BD 8C C0    LDA   $C08C,X
2171-   10 FB       BPL   $216E
2173-   C9 D4       CMP   #$D4
2175-   D0 B9       BNE   $2130

; increment target memory page
2177-   E6 84       INC   $84

; decrement sector count (initialized
; at $0132)
2179-   C6 86       DEC   $86
217B-   D0 DA       BNE   $2157

; exit via RTS
217D-   60          RTS

Wait, what? Ah, we're using the same
trick we used to call this routine --
the stack has been prefilled with a
series of "return" addresses. It's
time to "return" to the next one.

*21D0.

21D0- 2F 01 FF 03 FF 04 4F 04
            ^^^^^
     next return address

$03FF + 1 = $0400, and that's where I
get to interrupt the boot.

                   ~

               Chapter 5
   In Which Everything is Unfriendly
     I Mean Just Wildly Unfriendly
        Like You Think You Know
          But You Don't Know
      Just Way Over The Top, Man


*BLOAD TRACE3
.
. [same as previous trace]
.
; reproduce the decryption loop that
; was originally at $0320
9705-   84 48       STY   $48
9707-   A0 00       LDY   #$00
9709-   B9 00 03    LDA   $0300,Y
970C-   45 48       EOR   $48
970E-   99 00 01    STA   $0100,Y
9711-   C8          INY
9712-   D0 F5       BNE   $9709

; now that the stack is in place at
; $0100, change the first return
; address so it points to a callback
; under my control (instead of
; continuing to $0400)
9714-   A9 21       LDA   #$21
9716-   8D D2 01    STA   $01D2
9719-   A9 97       LDA   #$97
971B-   8D D3 01    STA   $01D3

; continue the boot
971E-   A2 CF       LDX   #$CF
9720-   9A          TXS
9721-   60          RTS

; (callback is here) copy the contents
; of the text page to higher memory
9722-   A2 04       LDX   #$04
9724-   A0 00       LDY   #$00
9726-   B9 00 04    LDA   $0400,Y
9729-   99 00 24    STA   $2400,Y
972C-   C8          INY
972D-   D0 F7       BNE   $9726
972F-   EE 28 97    INC   $9728
9732-   EE 2B 97    INC   $972B
9735-   CA          DEX
9736-   D0 EE       BNE   $9726

; turn off the drive and reboot to my
; work disk
9738-   AD E8 C0    LDA   $C0E8
973B-   4C 00 C5    JMP   $C500

*BSAVE TRACE4,A$9600,L$13E
*9600G
...reboots slot 6...
...reboots slot 5...

]BSAVE BOOT.0400-07FF,A$2400,L$400
]CALL -151

I'm going to leave this code at $2400,
since I can't put it on the text page
and examine it at the same time.
Relative branches will look correct,
but absolute addresses will be off by
$2000.

*2400L

; copy one of these four pages verbatim
; to the top of main memory
2400-   A0 00       LDY   #$00
2402-   B9 00 07    LDA   $0700,Y
2405-   99 00 BF    STA   $BF00,Y
2408-   C8          INY
2409-   D0 F7       BNE   $2402
240B-   20 DD 06    JSR   $06DD

*26DDL

; as far as I can tell, the only
; purpose of this subroutine is to
; corrupt the previous boot stage
26DD-   A0 0F       LDY   #$0F
26DF-   BE C0 07    LDX   $07C0,Y
26E2-   98          TYA
26E3-   9D 00 02    STA   $0200,X
26E6-   88          DEY
26E7-   10 F6       BPL   $26DF
26E9-   60          RTS

Continuing from $040E...

; copy boot slot
240E-   A6 2B       LDX   $2B
2410-   86 08       STX   $08
2412-   8E F1 BF    STX   $BFF1
2415-   20 50 07    JSR   $0750

*2750L

; wipe the language card, if any
2750-   AD 81 C0    LDA   $C081
2753-   AD 81 C0    LDA   $C081
2756-   A0 00       LDY   #$00
2758-   A9 D0       LDA   #$D0
275A-   84 00       STY   $00
275C-   85 01       STA   $01
275E-   B1 00       LDA   ($00),Y
2760-   91 00       STA   ($00),Y
2762-   C8          INY
2763-   D0 F9       BNE   $275E
2765-   E6 01       INC   $01
2767-   D0 F5       BNE   $275E
2769-   AD 80 C0    LDA   $C080
276C-   60          RTS

Continuing from $0418...

; set low-level reset vectors, page 3
; vectors, and even the input/output
; vectors, all to point to $BF00 --
; presumably The Badlands (from which
; there is no return)
2418-   AD 83 C0    LDA   $C083
241B-   AD 83 C0    LDA   $C083
241E-   A0 00       LDY   #$00
2420-   A9 BF       LDA   #$BF
2422-   8C FC FF    STY   $FFFC
2425-   8D FD FF    STA   $FFFD
2428-   8C F2 03    STY   $03F2
242B-   8D F3 03    STA   $03F3
242E-   A0 03       LDY   #$03
2430-   8C F0 03    STY   $03F0
2433-   8D F1 03    STA   $03F1
2436-   84 36       STY   $36
2438-   85 37       STA   $37
243A-   84 38       STY   $38
243C-   85 39       STA   $39
243E-   49 A5       EOR   #$A5
2440-   8D F4 03    STA   $03F4
2443-   A9 00       LDA   #$00
2445-   85 00       STA   $00
2447-   85 01       STA   $01

; exit via stack (again)
2449-   60          RTS

The next phase of the boot is waiting
for us on the stack. But before we get
to that, let's look at The Badlands:

*FE89G FE93G       ; disconnect DOS
*BF00<2700.27FFM   ; simulate copy loop

*BF00L

; There are multiple entry points here:
; $BF00, $BF03, $BF06, and $BF09
; (hidden in this listing by the "BIT"
; opcodes), each with a different ASCII
; character
BF00-   A9 D2       LDA   #$D2
BF02-   2C A9 D0    BIT   $D0A9
BF05-   2C A9 CC    BIT   $CCA9
BF08-   2C A9 A1    BIT   $A1A9
BF0B-   48          PHA

; wipe the language card again
; (not shown)
BF0C-   20 50 BF    JSR   $BF50

; TEXT/HOME/NORMAL
BF0F-   20 2F FB    JSR   $FB2F
BF12-   20 58 FC    JSR   $FC58
BF15-   20 84 FE    JSR   $FE84

; Depending on the initial entry point,
; this displays a different character
; in the top left corner of the screen
BF18-   68          PLA
BF19-   8D 00 04    STA   $0400

; now wipe all of main memory
BF1C-   A0 00       LDY   #$00
BF1E-   98          TYA
BF1F-   99 00 BE    STA   $BE00,Y
BF22-   C8          INY
BF23-   D0 FA       BNE   $BF1F
BF25-   CE 21 BF    DEC   $BF21

; while playing a sound
BF28-   2C 30 C0    BIT   $C030
BF2B-   AD 21 BF    LDA   $BF21
BF2E-   C9 08       CMP   #$08
BF30-   B0 EA       BCS   $BF1C

; munge the reset vector
BF32-   8D F3 03    STA   $03F3
BF35-   8D F4 03    STA   $03F4

; and reboot from whence we came
BF38-   AD F1 BF    LDA   $BFF1
BF3B-   4A          LSR
BF3C-   4A          LSR
BF3D-   4A          LSR
BF3E-   4A          LSR
BF3F-   09 C0       ORA   #$C0
BF41-   E9 00       SBC   #$00
BF43-   48          PHA
BF44-   A9 FF       LDA   #$FF
BF46-   48          PHA
BF47-   60          RTS

Yeah, let's try not to end up there.

                   ~

               Chapter 6
     In Which The Floodgates Open
     But Perhaps A Bit Prematurely
   Wait That Is Bad, That Is Not How
    Floodgates Are Supposed To Work


After a quick reboot (because I
disconnected DOS to examine the code
at $BF00), I'll continue tracing the
boot. $0449 exited via the stack again,
so let's see where that ends up.

*C500G
*BLOAD BOOT.0100-01FF,A$2100

*21D0.

21D0- 2F 01 FF 03 FF 04 4F 04
                  ^^^^^
           next return address

$04FF + 1 = $0500, and that's where
I can pick up the boot code.

*BLOAD BOOT.0400-07FF,A$2400
*2500L

2500-   A9 A0       LDA   #$A0
2502-   85 02       STA   $02
2504-   A9 02       LDA   #$02
2506-   48          PHA
2507-   20 00 06    JSR   $0600

*2600L

2600-   4C 5F 06    JMP   $065F

*265FL

; this is a standard track seek routine
; that takes the target phase (track*2)
; in the accumulator and remembers the
; current phase in $BFF0
265F-   85 41       STA   $41
2661-   CD F0 BF    CMP   $BFF0
2664-   F0 53       BEQ   $26B9
2666-   A9 00       LDA   #$00
2668-   85 26       STA   $26
266A-   AD F0 BF    LDA   $BFF0
266D-   85 27       STA   $27
266F-   38          SEC
2670-   E5 41       SBC   $41
2672-   F0 33       BEQ   $26A7
2674-   B0 07       BCS   $267D
2676-   49 FF       EOR   #$FF
2678-   EE F0 BF    INC   $BFF0
267B-   90 05       BCC   $2682
267D-   69 FE       ADC   #$FE
267F-   CE F0 BF    DEC   $BFF0

[...removed for brevity...]

26C4-   60          RTS

We call this subroutine with A=$02 (set
at $0504), so now we're on track 1.

Continuing from $050A...

250A-   20 60 05    JSR   $0560

*2560L

; $BFF0 is the current phase
2560-   AD F0 BF    LDA   $BFF0
2563-   4A          LSR
2564-   A2 03       LDX   #$03
2566-   29 0F       AND   #$0F
2568-   A8          TAY

; based on the current phase, set four
; locations in zero page at $50..$53
2569-   B9 D0 BF    LDA   $BFD0,Y
256C-   95 50       STA   $50,X
256E-   C8          INY
256F-   98          TYA
2570-   CA          DEX
2571-   10 F3       BPL   $2566

$BFD0 came from $07D0 which is in
memory at $27D0 and looks like this:

27D0- FB F6 F5 EB DF DE DB D5
27D8- BF BE BD BB BA B6 B5 AE

Those are all valid nibble values, so
I'm guessing these zero page addresses
are used in a read routine later.

Continuing from $0573...

2573-   A2 0C       LDX   #$0C
2575-   A9 30       LDA   #$30
2577-   4C 03 06    JMP   $0603

*2603L

2603-   86 3E       STX   $3E
2605-   85 3A       STA   $3A
2607-   A6 3E       LDX   $3E
2609-   86 40       STX   $40
260B-   A0 00       LDY   #$00
260D-   A5 3A       LDA   $3A
260F-   8C 41 06    STY   $0641
2612-   8D 42 06    STA   $0642
2615-   A6 08       LDX   $08

; reads a nibble (not shown)
2617-   20 59 06    JSR   $0659

; later is now -- compare to the first
; of the zero page addresses we just
; set at $056C
261A-   C5 50       CMP   $50
261C-   D0 F9       BNE   $2617

; and a second nibble, compare to the
; second zero page address
261E-   20 59 06    JSR   $0659
2621-   C5 51       CMP   $51
2623-   D0 F2       BNE   $2617

; and a third nibble, compare to the
; third zero page address
2625-   20 59 06    JSR   $0659
2628-   C5 52       CMP   $52
262A-   D0 EB       BNE   $2617

So every phase ends up with its own
unique three-nibble prologue.

; read data
262C-   BC 8C C0    LDY   $C08C,X
262F-   10 FB       BPL   $262C
2631-   B9 00 02    LDA   $0200,Y
2634-   0A          ASL
2635-   0A          ASL
2636-   0A          ASL
2637-   0A          ASL
2638-   BC 8C C0    LDY   $C08C,X
263B-   10 FB       BPL   $2638
263D-   19 00 02    ORA   $0200,Y

; self-modified earlier at $60F
2640-   8D 00 FF    STA   $FF00   ; /!\
2643-   EE 41 06    INC   $0641
2646-   D0 E4       BNE   $262C
2648-   EE 42 06    INC   $0642

; read epilogue nibble, compare to the
; fourth zero page address
264B-   BD 8C C0    LDA   $C08C,X
264E-   10 FB       BPL   $264B
2650-   C5 53       CMP   $53
2652-   D0 B3       BNE   $2607

Like the prologues, each phase ends up
with its own epilogue.

; decrement page count (set by X on
; entry, initially #$0C from $0573)
2654-   C6 40       DEC   $40
2656-   D0 D4       BNE   $262C
2658-   60          RTS

So we're reading #$0C pages into $3000,
then returning all the way to $050A.

*250DL

; copy the #$0C pages we just read into
; place, while computing two checksums
250D-   A0 00       LDY   #$00
250F-   A9 30       LDA   #$30
2511-   84 10       STY   $10
2513-   85 11       STA   $11

; look up target page (zero page $02
; initialized to #$A0 at $0500)
2515-   A6 02       LDX   $02
2517-   BD 00 05    LDA   $0500,X
251A-   84 12       STY   $12
251C-   85 13       STA   $13
251E-   B1 10       LDA   ($10),Y
2520-   91 12       STA   ($12),Y

; checksum 1
2522-   45 00       EOR   $00
2524-   85 00       STA   $00
2526-   B1 10       LDA   ($10),Y

; checksum 2
2528-   18          CLC
2529-   65 01       ADC   $01
252B-   85 01       STA   $01
252D-   C8          INY
252E-   D0 EE       BNE   $251E
2530-   E6 02       INC   $02
2532-   E6 13       INC   $13
2534-   E6 11       INC   $11
2536-   A5 11       LDA   $11
2538-   C9 3C       CMP   #$3C
253A-   90 D9       BCC   $2515

; advance track counter
253C-   68          PLA
253D-   18          CLC
253E-   69 02       ADC   #$02

; up to track $08 (phase $10)
2540-   C9 10       CMP   #$10
2542-   90 C2       BCC   $2506

; validate checksums
2544-   A5 00       LDA   $00
2546-   4D F2 BF    EOR   $BFF2
2549-   D0 08       BNE   $2553
254B-   A5 01       LDA   $01
254D-   4D F3 BF    EOR   $BFF3
2550-   D0 01       BNE   $2553

; if checksums match, exit via stack
2552-   60          RTS

; if checksums don't match, jump to
; The Badlands
2553-   4C 06 BF    JMP   $BF06

So the track read routine always reads
into $3000+, then the caller copies the
data into different target pages based
on the table at $05A0, while computing
two different checksums. Here are the
target pages:

*25A0.

25A0- 08 09 0A 0B 0C 0D 0E 0F
25A8- 10 11 12 13 14 15 16 17
25B0- 18 19 1A 1B 1C 1D 1E 1F
25B8- 20 21 22 23 24 25 26 60
25C0- 61 62 63 64 65 66 67 68
25C8- 69 6A 6B 6C 6D 6E 6F 70
25D0- 71 72 73 74 75 76 77 78
25D8- 79 7A 7B 7C 7D 7E 7F 80
25E0- 81 82 83 84 85 86 87 88
25E8- 89 8A 8B 8C 8D B8 B9 BA
25F0- BB BC BD BE 00 00 00 00

It looks like there are three distinct
chunks, one starting at $800, one at
$6000, and one at $B800. Then, assuming
both checksums validate, we exit via
the stack to...

*BLOAD BOOT.0100-01FF,A$2100

*21D0.

21D0- 2F 01 FF 03 FF 04 4F 04
                        ^^^^^

...$0450.

But first, let's capture all of that.

*BLOAD TRACE4

; same as previous traces
96F8-   A9 05       LDA   #$05
96FA-   8D 38 08    STA   $0838
96FD-   A9 97       LDA   #$97
96FF-   8D 39 08    STA   $0839
9702-   4C 01 08    JMP   $0801
9705-   84 48       STY   $48
9707-   A0 00       LDY   #$00
9709-   B9 00 03    LDA   $0300,Y
970C-   45 48       EOR   $48
970E-   99 00 01    STA   $0100,Y
9711-   C8          INY
9712-   D0 F5       BNE   $9709

; now that the stack is in place at
; $0100, change the return address at
; $01D6 so it points to a callback
; under my control instead of
; continuing to $0450)
9714-   A9 21       LDA   #$21
9716-   8D D6 01    STA   $01D6
9719-   A9 97       LDA   #$97
971B-   8D D7 01    STA   $01D7

; continue the boot
971E-   A2 CF       LDX   #$CF
9720-   9A          TXS
9721-   60          RTS

; (callback is here) copy the contents
; of page 8 to higher memory
; The rest of memory that we care about
; will be undisturbed by rebooting to
; my work disk (it only clobbers page 8
; and page $BF and loads DOS directly
; into the language card)
9722-   A0 00       LDY   #$00
9724-   B9 00 08    LDA   $0800,Y
9727-   99 00 28    STA   $2800,Y
972A-   C8          INY
972B-   D0 F7       BNE   $9724
972D-   AD 82 C0    LDA   $C082

; turn off the drive and reboot to my
; work disk
9730-   AD E8 C0    LDA   $C0E8
9733-   4C 00 C5    JMP   $C500

*BSAVE TRACE5,A$9600,L$136
*9600G
...reboots slot 6...
...reboots slot 5...

]CALL -151

; restore memory we moved during trace
*800<2800.28FFM

; save captured memory in three chunks
*BSAVE OBJ.0800-26FF,A$800,L$1F00
*BSAVE OBJ.6000-8DFF,A$6000,L$2E00
*BSAVE OBJ.B800-BEFF,A$B800,L$700

That feels like progress.

                   ~

               Chapter 7
           In Which We Make
        A Disturbing Discovery


Continuing the boot trace from $0450...

*BLOAD BOOT.0400-07FF,A$2400

*2450L

; seek to track $22.5 (!)
2450-   A9 45       LDA   #$45
2452-   20 00 06    JSR   $0600
2455-   A9 40       LDA   #$40
2457-   85 00       STA   $00
2459-   88          DEY
245A-   D0 04       BNE   $2460
245C-   C6 00       DEC   $00

; note: countdown (on read failure)
; does not jump to The Badlands, it
; just continues later in this routine,
; so whatever this is doing, it's not
; fatal if it doesn't work
245E-   F0 48       BEQ   $24A8

; find a three-nibble prologue
2460-   BD 8C C0    LDA   $C08C,X
2463-   10 FB       BPL   $2460
2465-   C9 D5       CMP   #$D5
2467-   D0 F0       BNE   $2459
2469-   BD 8C C0    LDA   $C08C,X
246C-   10 FB       BPL   $2469
246E-   C9 FF       CMP   #$FF
2470-   D0 F3       BNE   $2465
2472-   BD 8C C0    LDA   $C08C,X
2475-   10 FB       BPL   $2472
2477-   C9 DD       CMP   #$DD
2479-   D0 F3       BNE   $246E

; read a page of data
247B-   A0 00       LDY   #$00
247D-   A0 00       LDY   #$00
247F-   BD 8C C0    LDA   $C08C,X
2482-   10 FB       BPL   $247F
2484-   38          SEC
2485-   2A          ROL
2486-   85 00       STA   $00
2488-   EA          NOP
2489-   BD 8C C0    LDA   $C08C,X
248C-   10 FB       BPL   $2489
248E-   25 00       AND   $00
2490-   99 00 07    STA   $0700,Y
2493-   C8          INY
2494-   D0 E9       BNE   $247F

; match a one-nibble epilogue
2496-   BD 8C C0    LDA   $C08C,X
2499-   10 FB       BPL   $2496
249B-   C9 D5       CMP   #$D5
249D-   D0 09       BNE   $24A8

; copy the page into $2600 (note: this
; will clobber memory we already read
; from a different track in the
; previous stage)
249F-   B9 00 07    LDA   $0700,Y
24A2-   99 00 26    STA   $2600,Y
24A5-   C8          INY
24A6-   D0 F7       BNE   $249F

; execution continues here regardless
24A8-   A0 FF       LDY   #$FF
24AA-   B9 00 02    LDA   $0200,Y
24AD-   99 00 BD    STA   $BD00,Y
24B0-   88          DEY
24B1-   30 F7       BMI   $24AA

; exit via the stack (why am I not
; surprised)
24B3-   60          RTS

OK, so what's going on here? We're
reading exactly one page of memory from
track $22.5, and clobbering a page we
already read, but we don't care if it
doesn't work?

Examining the existing data at $2600
(that might get clobbered), it becomes
clear:

*BLOAD OBJ.0800-26FF,A$800
*2600.

[...]

It's a mix of data and code, but it
includes five readable ASCII strings:

                 --v--

1.       000000
2.       000000
3.       000000
4.       000000
5.       000000

                 --^--

Oh, these are the high scores (and the
high score-related input code, later in
the page). In the previous boot stage,
it reads a set of default scores. If
this routine can read saved scores from
track $22.5, it will patch them in, but
no problem if it can't.

On a personal note, I had ABSOLUTELY NO
IDEA that this game saved high scores
on disk. The classic crack I played
growing up did not do that.

Obviously, my crack will do that.

Also, there's some more copying that
clobbers already-read parts of memory
(copying from $0280+ to $BD80+) that my
previous captures didn't account for.
So I'm going to capture it again but
wait until after this routine at $0450
executes.

*BLOAD TRACE5
;
; ...same, same...
;
; now that the stack is in place at
; $0100, change the return address at
; $01D8 so it points to a callback
; under my control
9714-   A9 21       LDA   #$21
9716-   8D D8 01    STA   $01D8
9719-   A9 97       LDA   #$97
971B-   8D D9 01    STA   $01D9

; continue the boot
971E-   A2 CF       LDX   #$CF
9720-   9A          TXS
9721-   60          RTS

; (callback is here)
9722-   A0 00       LDY   #$00
9724-   B9 00 08    LDA   $0800,Y
9727-   99 00 28    STA   $2800,Y
972A-   C8          INY
972B-   D0 F7       BNE   $9724
972D-   AD 82 C0    LDA   $C082

; turn off the drive and reboot to my
; work disk
9730-   AD E8 C0    LDA   $C0E8
9733-   4C 00 C5    JMP   $C500

*9600G
...reboots slot 6...
...reboots slot 5...

]CALL -151

*800<2800.28FFM
*BSAVE OBJ.0800-26FF,A$800,L$1F00
*BSAVE OBJ.6000-8DFF,A$6000,L$2E00
*BSAVE OBJ.B800-BEFF,A$B800,L$700

Now *that's* what I call progress.

                   ~

               Chapter 8
    In Which All Roads Lead To Rome
    And By Rome I Mean A Jump Table
    At The Top Of Main Memory Which
    Is A Weird Thing To Mean By Rome
            But Here We Are


After exiting via the stack at $04B3,
the game "returns" to...

*BLOAD BOOT.0100-01FF,A$2100

*21D0.

21D0- 2F 01 FF 03 FF 04 4F 04
21D8- FF 1F 85 4B 4C B7 E5 86
      ^^^^^

...$1FFF + 1 = $2000, which I have
already captured but just partially
clobbered, so let's reload that.

*BLOAD OBJ.0800-26FF,A$800

*2000L

; relocate $2100..$26FF to $AF00..$B4FF
2000-   EA          NOP
2001-   EA          NOP
2002-   EA          NOP
2003-   A9 21       LDA   #$21
2005-   8D 17 20    STA   $2017
2008-   A9 AF       LDA   #$AF
200A-   8D 1A 20    STA   $201A
200D-   A9 00       LDA   #$00
200F-   8D 16 20    STA   $2016
2012-   8D 19 20    STA   $2019
2015-   AD FF FF    LDA   $FFFF
2018-   8D FF FF    STA   $FFFF
201B-   EE 19 20    INC   $2019
201E-   EE 16 20    INC   $2016
2021-   D0 F2       BNE   $2015
2023-   EE 1A 20    INC   $201A
2026-   EE 17 20    INC   $2017
2029-   AD 17 20    LDA   $2017
202C-   C9 27       CMP   #$27
202E-   D0 E5       BNE   $2015
2030-   EA          NOP
2031-   EA          NOP
2032-   EA          NOP

; relocate a small chunk to $0300
2033-   A2 00       LDX   #$00
2035-   BD 70 20    LDA   $2070,X
2038-   9D 00 03    STA   $0300,X
203B-   E8          INX
203C-   E0 90       CPX   #$90
203E-   90 F5       BCC   $2035

; test if there is a joystick installed
2040-   AD 70 C0    LDA   $C070
2043-   A2 08       LDX   #$08
2045-   AD 64 C0    LDA   $C064
2048-   0D 65 C0    ORA   $C065
204B-   10 15       BPL   $2062
204D-   88          DEY
204E-   D0 F5       BNE   $2045
2050-   CA          DEX
2051-   D0 F2       BNE   $2045

; if no joystick, self-modify some code
2053-   A9 B0       LDA   #$B0
2055-   8D 46 08    STA   $0846
2058-   A9 10       LDA   #$10
205A-   8D 47 08    STA   $0847
205D-   A9 00       LDA   #$00
205F-   8D 07 80    STA   $8007

; either way, exit via $19BC
2062-   4C BC 19    JMP   $19BC

*19BCL

19BC-   A9 01       LDA   #$01
19BE-   8D 55 80    STA   $8055
19C1-   AD 55 80    LDA   $8055
19C4-   F0 10       BEQ   $19D6
19C6-   A9 0C       LDA   #$0C
19C8-   8D AC 19    STA   $19AC
19CB-   4C 20 18    JMP   $1820

*1820L

; weird little compare that potentially
; jumps over some subroutine calls
; (will come back to this)
1820-   AD 80 95    LDA   $9580
1823-   C9 8D       CMP   #$8D
1825-   F0 0B       BEQ   $1832
1827-   A9 00       LDA   #$00
1829-   8D AB 19    STA   $19AB
182C-   20 10 18    JSR   $1810
182F-   20 00 18    JSR   $1800
1832-   20 00 96    JSR   $9600

There's nothing loaded at $9600, so one
of the routines at $1800 or $1810 must
be loading additional code from disk.

*1810L

1810-   A9 01       LDA   #$01
1812-   8D B4 18    STA   $18B4
1815-   AD AB 19    LDA   $19AB
1818-   20 FA BF    JSR   $BFFA
...

*1800L

1800-   A9 01       LDA   #$01
1802-   8D B4 18    STA   $18B4
1805-   AD AC 19    LDA   $19AC
1808-   18          CLC
1809-   69 06       ADC   #$06
180B-   20 FA BF    JSR   $BFFA

It seems that all roads lead to $BFFA,
which was copied from $0700 (at $0400).

*BLOAD OBJ.0400-07FF,A$2C00
*2FFAL

2FFA-   4C 00 BC    JMP   $BC00
2FFD-   4C 00 BB    JMP   $BB00

$BC00 and $BB00 were both read directly
from disk by the read routine that took
target pages from the table at $05A0. I
have those in a separate file.

                   ~

               Chapter 9
     In Which We Flutter For A Day
        And Think It Is Forever


*BLOAD BOOT.B800-BEFF,A$B800
*BC00L

BC00-   48          PHA

; turns on the drive and waits for it
; to spin up (not shown)
BC01-   20 E5 BE    JSR   $BEE5
BC04-   68          PLA

; The accumulator is some sort of ID
; number to this routine. Values less
; than 6 get routed to $BC0E, and
; higher values get reduced by 6 and
; routed to $BCD0.
BC05-   C9 06       CMP   #$06
BC07-   90 05       BCC   $BC0E
BC09-   E9 06       SBC   #$06
BC0B-   4C D0 BC    JMP   $BCD0

; calculate (A*2)+$10
BC0E-   85 60       STA   $60
BC10-   A5 60       LDA   $60
BC12-   0A          ASL
BC13-   69 10       ADC   #$10

; seek to that phase (not shown)
BC15-   20 00 BE    JSR   $BE00

So if A=$00 on entry, we end up on
track $08. A=$01 -> track $09, and so
forth, up to A=$05 -> track $0D.
(Higher values of A get routed to an
entirely different routine at $BCD0.)

BC18-   A5 60       LDA   $60
BC1A-   0A          ASL
BC1B-   0A          ASL
BC1C-   85 61       STA   $61
BC1E-   A9 D4       LDA   #$D4
BC20-   85 43       STA   $43
BC22-   20 70 BC    JSR   $BC70

*BC70L

BC70-   A0 00       LDY   #$00
BC72-   A9 8E       LDA   #$8E
BC74-   84 62       STY   $62
BC76-   48          PHA
BC77-   20 A0 BC    JSR   $BCA0

*BCA0L

BCA0-   48          PHA
BCA1-   A5 61       LDA   $61
BCA3-   29 07       AND   #$07
BCA5-   A8          TAY
BCA6-   B9 C0 BC    LDA   $BCC0,Y
BCA9-   85 40       STA   $40
BCAB-   A5 61       LDA   $61
BCAD-   4A          LSR
BCAE-   09 AA       ORA   #$AA
BCB0-   85 41       STA   $41
BCB2-   A5 61       LDA   $61
BCB4-   09 AA       ORA   #$AA
BCB6-   85 42       STA   $42
BCB8-   68          PLA
BCB9-   E6 61       INC   $61
BCBB-   4C 90 BE    JMP   $BE90

This is a lot of setup, but the result
is that zero page $40, $41, and $42
contain a track-specific sequence of
three valid nibble values. Here is the
table at $BCC0 that is read at $BCA6:

*BCC0.BCC7

BCC0- D5 B5 B7 BC DF D4 B4 DB

One of those goes directly into zero
page $40. Another nibble based on the
track*2 (in $61) goes into $41. A
third, based on the track*4, goes into
$42. And zero page $43 is a fourth
valid nibble, always #$D4 (set at
$BC20). And now we continue from $BE90:

*BE90L

; read $200 bytes of 4-and-4-encoded
; data from the current track into
; $8E00 (high byte comes from A which
; was set to a constant at $BC72)
BE90-   85 3A       STA   $3A
BE92-   A2 02       LDX   #$02
BE94-   86 3D       STX   $3D
BE96-   A0 00       LDY   #$00
BE98-   A5 3A       LDA   $3A
BE9A-   84 3B       STY   $3B
BE9C-   85 3C       STA   $3C
BE9E-   AE F1 BF    LDX   $BFF1

; find 3-nibble prologue (different
; for each track)
BEA1-   BD 8C C0    LDA   $C08C,X
BEA4-   10 FB       BPL   $BEA1
BEA6-   C5 40       CMP   $40
BEA8-   D0 F7       BNE   $BEA1
BEAA-   BD 8C C0    LDA   $C08C,X
BEAD-   10 FB       BPL   $BEAA
BEAF-   C5 41       CMP   $41
BEB1-   D0 F3       BNE   $BEA6
BEB3-   BD 8C C0    LDA   $C08C,X
BEB6-   10 FB       BPL   $BEB3
BEB8-   C5 42       CMP   $42
BEBA-   D0 F3       BNE   $BEAF

; read $100 bytes of 4-and-4-encoded
; data
BEBC-   BD 8C C0    LDA   $C08C,X
BEBF-   10 FB       BPL   $BEBC
BEC1-   2A          ROL
BEC2-   85 3E       STA   $3E
BEC4-   BD 8C C0    LDA   $C08C,X
BEC7-   10 FB       BPL   $BEC4
BEC9-   25 3E       AND   $3E
BECB-   91 3B       STA   ($3B),Y
BECD-   C8          INY
BECE-   D0 EC       BNE   $BEBC
BED0-   0E 00 C0    ASL   $C000

; verify 1-nibble epilogue (constant)
BED3-   BD 8C C0    LDA   $C08C,X
BED6-   10 FB       BPL   $BED3
BED8-   C5 43       CMP   $43
BEDA-   D0 B6       BNE   $BE92

; increment target page and decrement
; counter
BEDC-   E6 3C       INC   $3C
BEDE-   C6 3D       DEC   $3D
BEE0-   D0 DA       BNE   $BEBC
BEE2-   60          RTS

Continuing from $BC7A...

BC7A-   A4 62       LDY   $62
BC7C-   18          CLC

; get current phase
BC7D-   AD F0 BF    LDA   $BFF0

; add from this table
BC80-   79 98 BC    ADC   $BC98,Y

; seek to that phase, but with a twist
BC83-   20 03 BE    JSR   $BE03

; A=A+2 (eventually passed into $BE90
; as the target page in memory)
BC86-   68          PLA
BC87-   18          CLC
BC88-   69 02       ADC   #$02

; do all of this 4 times
BC8A-   A4 62       LDY   $62
BC8C-   C8          INY
BC8D-   C0 04       CPY   #$04
BC8F-   90 E3       BCC   $BC74
BC91-   60          RTS

So we're reading $200 bytes, 4 times.

Now let's look at what's at $BE03.

Here is the seek routine, starting at
$BE00 (as called from $BC15):

*BE00L

BE00-   A2 13       LDX   #$13
BE02-   2C A2 0A    BIT   $0AA2
BE05-   8E 66 BE    STX   $BE66
BE08-   85 3A       STA   $3A
BE0A-   CD F0 BF    CMP   $BFF0
BE0D-   F0 52       BEQ   $BE61
BE0F-   A9 00       LDA   #$00
BE11-   85 3B       STA   $3B
BE13-   AD F0 BF    LDA   $BFF0
BE16-   85 3C       STA   $3C
BE18-   38          SEC
BE19-   E5 3A       SBC   $3A
BE1B-   F0 33       BEQ   $BE50
BE1D-   B0 07       BCS   $BE26
BE1F-   49 FF       EOR   #$FF
BE21-   EE F0 BF    INC   $BFF0
BE24-   90 05       BCC   $BE2B
BE26-   69 FE       ADC   #$FE
BE28-   CE F0 BF    DEC   $BFF0
BE2B-   C5 3B       CMP   $3B
BE2D-   90 02       BCC   $BE31
BE2F-   A5 3B       LDA   $3B
BE31-   C9 0C       CMP   #$0C
BE33-   B0 01       BCS   $BE36
BE35-   A8          TAY
BE36-   38          SEC
BE37-   20 54 BE    JSR   $BE54
BE3A-   B9 70 BE    LDA   $BE70,Y
BE3D-   20 65 BE    JSR   $BE65
BE40-   A5 3C       LDA   $3C
BE42-   18          CLC
BE43-   20 57 BE    JSR   $BE57
BE46-   B9 7C BE    LDA   $BE7C,Y
BE49-   20 65 BE    JSR   $BE65
BE4C-   E6 3B       INC   $3B
BE4E-   D0 C3       BNE   $BE13
BE50-   20 65 BE    JSR   $BE65
BE53-   18          CLC
BE54-   AD F0 BF    LDA   $BFF0
BE57-   29 03       AND   #$03
BE59-   2A          ROL
BE5A-   0D F1 BF    ORA   $BFF1
BE5D-   AA          TAX
BE5E-   BD 80 C0    LDA   $C080,X
BE61-   AE F1 BF    LDX   $BFF1
BE64-   60          RTS
BE65-   A2 13       LDX   #$13
BE67-   CA          DEX
BE68-   D0 FD       BNE   $BE67
BE6A-   38          SEC
BE6B-   E9 01       SBC   #$01
BE6D-   D0 F6       BNE   $BE65
BE6F-   60          RTS

This is essentially identical to what
DOS 3.3 does. It keeps the current
phase in $BFF0 and hits the appropriate
stepper motors in a specific cadence to
move backward or forward on the disk.
The cadence is set by the wait loop at
$BE65, because floppy drives are analog
and nothing happens instantaneously.

This is all very normal...

...except that there is a second entry
point, at $BE03, hidden within the BIT
instruction.

*BE03L

BE03-   A2 0A       LDX   #$0A
BE05-   8E 66 BE    STX   $BE66

In this entry point, the wait time in
the wait loop (at $BE65) is reduced
from #$13 to #$0A. This is extremely
unusual.

What does it even mean to change the
timing of a drive seek routine? It
means that, after we hit the stepper
motor to tell the drive head to start
moving in one direction, we're not
waiting very long for it to settle on
the next "stopping point," which is the
next full phase.  If we were on track
$08, seeking one phase forward should
get us to track $08.5. But with the
reduced timing in this low-level wait
routine, we might be on $08.4 or $08.6.

It's all analog, baby.

In practice, this shouldn't matter too
much, because the drive head is wide
enough that it will pick up the data
that was originally written to track
$08.5. But it is very unusual to see
any variation at all of the standard
drive seek code.

Wait, it gets worse.

In between each read of $200 bytes,
we're doing this at $BC7D:

BC7D-   AD F0 BF    LDA   $BFF0
BC80-   79 98 BC    ADC   $BC98,Y
BC83-   20 03 BE    JSR   $BE03

Here is the table at BC98:

BC98- 01 FF 01 00

This array is the differential to get
the drive to seek forward or backward.
Assume we initially seek to track $08.
The first time through this loop, we
read two sectors into $8E00..$8FFF,
then seek to phase+1 (because $BC98 is
$01) and end up on track $08.5.

The second time through the loop, we
read two sectors from track $08.5, into
$9000..$91FF. Then we seek to phase-1
(because $BC99 is $FF), back to track
$08.

The third time, we read two sectors
from track $08 into $9200..$93FF, then
seek to phase+1 again (because $BC9A
is $01), and we're back on track $08.5.

The fourth and final time, we read the
final two sectors from track $08.5 into
$9400..$95FF.

This is completely insane. The read
pattern looks like this:

 7.5     8.0     8.5     9.0     9.5
--+-------+-------+-------+-------+----
  .      8E00     .       .       .
  .      8F00     .       .       .
  .       .   \   .       .       .
  .       .      9000     .       .
  .       .      9100     .       .
  .       .   /   .       .       .
  .      9200     .       .       .
  .      9300     .       .       .
  .       .   \   .       .       .
  .       .      9400     .       .
  .       .      9500     .       .

This explains the little "fluttering"
noise the original disk makes during
this phase of the boot. It's seeking
back and forth between adjacent half
tracks, reading two sectors from each,
and repeating.

Boy am I glad I'm not trying to copy
this disk with a generic bit copier.
That would be nearly impossible, even
if I knew exactly which tracks were
split like this.

Continuing from $BC25...

; with $800 bytes now read into $8E00+,
; compute two checksums on the data
BC25-   A0 00       LDY   #$00
BC27-   84 40       STY   $40
BC29-   84 41       STY   $41
BC2B-   A9 8E       LDA   #$8E
BC2D-   84 42       STY   $42
BC2F-   85 43       STA   $43
BC31-   A2 08       LDX   #$08
BC33-   B1 42       LDA   ($42),Y
BC35-   45 40       EOR   $40
BC37-   85 40       STA   $40
BC39-   B1 42       LDA   ($42),Y
BC3B-   18          CLC
BC3C-   65 41       ADC   $41
BC3E-   85 41       STA   $41
BC40-   C8          INY
BC41-   D0 F0       BNE   $BC33
BC43-   E6 43       INC   $43
BC45-   CA          DEX
BC46-   D0 EB       BNE   $BC33

; validate checksums against tables of
; expected values
BC48-   A4 60       LDY   $60
BC4A-   A5 40       LDA   $40
BC4C-   59 E0 BF    EOR   $BFE0,Y
BC4F-   D0 0A       BNE   $BC5B
BC51-   A5 41       LDA   $41
BC53-   59 E6 BF    EOR   $BFE6,Y
BC56-   D0 03       BNE   $BC5B
BC58-   4C F0 BE    JMP   $BEF0

; if either checksum fails, play a tone
; and retry.
BC5B-   A9 CC       LDA   #$CC
BC5D-   A0 30       LDY   #$30
BC5F-   2C 30 C0    BIT   $C030
BC62-   AA          TAX
BC63-   CA          DEX
BC64-   D0 FD       BNE   $BC63
BC66-   88          DEY
BC67-   D0 F6       BNE   $BC5F
BC69-   4A          LSR
BC6A-   D0 F1       BNE   $BC5D
BC6C-   F0 A2       BEQ   $BC10

; otherwise exit via $BEF0, which turns
; off the drive and returns to the
; caller
BEF0-   AE F1 BF    LDX   $BFF1
BEF3-   BD 88 C0    LDA   $C088,X
BEF6-   60          RTS

                   ~

              Chapter 10
      A Fun Interlude, Much Like
  The Cartoons Featured In This Game
  Although Probably Not Quite As Fun
          If I'm Being Honest


I want to return to this weird little
compare at $1820:

1820-   AD 80 95    LDA   $9580
1823-   C9 8D       CMP   #$8D
1825-   F0 0B       BEQ   $1832 --+
1827-   A9 00       LDA   #$00    |
1829-   8D AB 19    STA   $19AB   |
182C-   20 10 18    JSR   $1810   |
182F-   20 00 18    JSR   $1800   |
1832-   20 00 96    JSR   $9600 <-+

As we have seen, $1810 and $1800 load
two chunks from disk, presumably the
ones that control the animated title
screen and demo that run on startup.
$9580 is part of the $800-byte chunk
that is flutter-read into $8E00.

These screens will repeat if you wait
long enough. When it's time to display
the title screen a second time, this
code checks to see if those chunks are
already in memory. If so, it skips
reloading them from disk and continues
from $1832 to display the title screen
again.

There's only one problem: the value at
$9580 is never initialized. Many games
clear main memory during boot as an
anti-debugging technique, but this one
never does. So $9580 could potentially
be $8D just by chance, if some other
program had been in memory before. In
which case, the game would skip loading
the title screen code the first time,
then crash trying to call it.

I can easily test this theory.

*9580:8D
*9600:00
*C600G
...game crashes, displays P, reboots...

Ha! Even better, the game's other anti-
debugging techniques kicked in. Putting
the magic value ($8D) at $9580 skips
the load of the title code, and putting
$00 (BRK) at $9600 triggers the BRK
handler, which the game traps and
redirects to The Badlands (chapter 5).
And since The Badlands does wipe main
memory, the game works properly after
the reboot, because the magic value has
been cleared.

This is a 40-year-old bug.

                   ~

              Chapter 11
         You Get A Half Track,
       And You Get A Half Track,
       And You Get A Half Track


Continuing to trace this routine at
$BC00... What if A is 6 (or higher)?
Then we set A=A-6 and jump to $BCD0,
where further wonders(*) no doubt await
us.

(*) not guaranteed, actual wonder may
    vary

*BCD0L

; use A-6 as an index into arrays
BCD0-   A8          TAY
BCD1-   A9 96       LDA   #$96
BCD3-   85 40       STA   $40
BCD5-   B9 70 BF    LDA   $BF70,Y
BCD8-   85 41       STA   $41
BCDA-   B9 71 BF    LDA   $BF71,Y
BCDD-   85 42       STA   $42

; seek to a track based on the table at
; $BF70
BCDF-   A5 41       LDA   $41
BCE1-   20 00 BE    JSR   $BE00

This is the table at $BF70, in memory
now at $2F00:

*2F70.

2F70- 1D 1F 21 25 27 2B 2F 31
2F78- 33 35 37 3B 3F 43 45 00

Remember, those are phases, so the
track is half of that. Phase $1D is
track $0E.5. All of them are odd, so
all of them resolve to half tracks.
Sure, why not. You get a half track,
and you get a half track, and you get a
half track. Half tracks for everyone.

Continuing from $BCE4...

BCE4-   20 60 BD    JSR   $BD60

*BD60L

BD60-   AD F0 BF    LDA   $BFF0
BD63-   4A          LSR
BD64-   A2 03       LDX   #$03
BD66-   29 0F       AND   #$0F
BD68-   A8          TAY
BD69-   B9 D0 BF    LDA   $BFD0,Y
BD6C-   95 50       STA   $50,X
BD6E-   C8          INY
BD6F-   98          TYA
BD70-   CA          DEX
BD71-   10 F3       BPL   $BD66
BD73-   EA          NOP
BD74-   EA          NOP
BD75-   EA          NOP
BD76-   A2 0C       LDX   #$0C
BD78-   A5 40       LDA   $40
BD7A-   4C 00 BD    JMP   $BD00

We are once again setting up zero page
(this time at $50+) with a sequence of
valid nibbles. Here is the 16-byte
table at $BFD0, in memory at $2FD0:

*2FD0.

2FD0- F7 ED EB EA DE DD DB D7
2FD8- D6 D5 BF BE BD BB BA AE

Then we continue to $BD00:

*BD00L

; X is always $0C here (set at $BD76)
BD00-   86 59       STX   $59
BD02-   85 56       STA   $56
BD04-   A6 59       LDX   $59
BD06-   86 5A       STX   $5A
BD08-   A0 00       LDY   #$00
BD0A-   A5 56       LDA   $56
BD0C-   8C 3F BD    STY   $BD3F
BD0F-   8D 40 BD    STA   $BD40
BD12-   AE F1 BF    LDX   $BFF1

; subroutine at $BD57 just reads a
; nibble in the usual way (not shown)
; so we're matching a 3-nibble prologue
BD15-   20 57 BD    JSR   $BD57
BD18-   C5 50       CMP   $50
BD1A-   D0 F9       BNE   $BD15
BD1C-   20 57 BD    JSR   $BD57
BD1F-   C5 51       CMP   $51
BD21-   D0 F2       BNE   $BD15
BD23-   20 57 BD    JSR   $BD57
BD26-   C5 52       CMP   $52
BD28-   D0 EB       BNE   $BD15

; read $100 bytes of encoded data
BD2A-   BC 8C C0    LDY   $C08C,X
BD2D-   10 FB       BPL   $BD2A
BD2F-   B9 00 BD    LDA   $BD00,Y
BD32-   0A          ASL
BD33-   0A          ASL
BD34-   0A          ASL
BD35-   0A          ASL
BD36-   BC 8C C0    LDY   $C08C,X
BD39-   10 FB       BPL   $BD36
BD3B-   19 00 BD    ORA   $BD00,Y

; this address was self-modified at
; $BD0C
BD3E-   8D 00 FF    STA   $FF00
BD41-   EE 3F BD    INC   $BD3F
BD44-   D0 E4       BNE   $BD2A

; increment target page
BD46-   EE 40 BD    INC   $BD40

; verify 1-nibble epilogue
BD49-   BD 8C C0    LDA   $C08C,X
BD4C-   10 FB       BPL   $BD49
BD4E-   C5 53       CMP   $53
BD50-   D0 B2       BNE   $BD04

; decrement sector counter and loop
BD52-   C6 5A       DEC   $5A
BD54-   D0 D4       BNE   $BD2A
BD56-   60          RTS

X is always $0C on entry, and A is
always $96, so we're always reading
$0C00 bytes into $9600+, all from the
same track.

Continuing from $BCE7...

; add $0C to the starting page and 2 to
; the target phase, then loop
BCE7-   18          CLC
BCE8-   A5 40       LDA   $40
BCEA-   69 0C       ADC   #$0C
BCEC-   85 40       STA   $40
BCEE-   A5 41       LDA   $41
BCF0-   69 02       ADC   #$02
BCF2-   85 41       STA   $41
BCF4-   C5 42       CMP   $42
BCF6-   D0 E9       BNE   $BCE1

; exit via $BEF0 which turns off the
; drive motor
BCF8-   4C F0 BE    JMP   $BEF0

This is a lot simpler than fluttering
between half tracks, and it's also more
flexible. The table of start and end
phases at $BF70 does not increment
consistently. In some cases, we end up
reading $0C00 bytes into $9600..$A1FF
and exiting. In other cases, we
increment the page and the track and
loop back to read another $0C00 bytes
into $A200..$ADFF.

$BC00 is basically a read_game_block()
function. Some game blocks are $0800
bytes long and read (in a flutter
pattern) into $8E00..$95FF. Some are
$0C00 bytes long and read (from a
single track, such as track $0E.5) into
$9600..$A1FF. Others are $1800 bytes
long and read (from two tracks) into
$9600..$ADFF.

Now let's go all the way back to the
game code at $19BC, removing all the
irrelevant details and focusing on what
gets passed into the read_game_block()
routine:

19C6-   A9 0C       LDA   #$0C
19C8-   8D AC 19    STA   $19AC
...
1827-   A9 00       LDA   #$00
1829-   8D AB 19    STA   $19AB
...

; read game block $00 into $8E00
1815-   AD AB 19    LDA   $19AB
1818-   20 FA BF    JSR   $BFFA
...

; read game block $12 ($0C+$06)
; into $9600
1805-   AD AC 19    LDA   $19AC
1808-   18          CLC
1809-   69 06       ADC   #$06
180B-   20 FA BF    JSR   $BFFA
...

; call the code we just read from game
; block $12, which displays the title
; screen
1832-   20 00 96    JSR   $9600

That is a very elegant system.

                   ~

              Chapter 12
          It's Raining Blocks


Based on the phase table at $BF70, the
disk has 20 distinct game blocks.
$00..$05 route through the flutter-read
routine at $BC0E, and $06..$13 branch
to $BCD0.

Capturing each block is made trickier
by the fact that there are data tables
at $BFD0 that are required in order to
read the higher game blocks.

Thus, a small chunk of new code:

*800L

; swap in game code at $BF00
0800-   20 15 08    JSR   $0815

; turn on drive
0803-   20 E5 BE    JSR   $BEE5

; seek to track $23.5
0806-   A9 45       LDA   #$45
0808-   8D F0 BF    STA   $BFF0

; seek to track $00
080B-   A9 00       LDA   #$00
080D-   20 00 BE    JSR   $BE00

; read game block $00
0810-   A9 00       LDA   #$00
0812-   20 00 BC    JSR   $BC00

; fall through to restore DOS code
; at $BF00
0815-   A0 00       LDY   #$00
0817-   BE 00 BF    LDX   $BF00,Y
081A-   B9 00 1F    LDA   $1F00,Y
081D-   99 00 BF    STA   $BF00,Y
0820-   8A          TXA
0821-   99 00 1F    STA   $1F00,Y
0824-   C8          INY
0825-   D0 F0       BNE   $0817
0827-   60          RTS

*BSAVE READBLOCK,A$800,L$28

$0811 holds the game block ID. The two
track seeks are to reset the drive to a
known state and sync up the game RWTS's
internal marker of where the drive head
is. This program has no idea where game
blocks end up in memory, but we've
already figured that out.

; put game's $BF00 at $1F00
; (will be swapped in/out)
*BLOAD BOOT.0400-07FF,A$1C00

; load the rest of the game RWTS
; in place
*BLOAD OBJ.B800-BEFF,A$B800

Armed and ready.

; capture game block $00
*800G
[...grind grind read read...]
*BSAVE BLOCK.00,A$8E00,L$800

; capture game block $01
*811:01 N 800G
*BSAVE BLOCK.01,A$8E00,L$800

; capture game block $02
*811:02 N 800G
*BSAVE BLOCK.02,A$8E00,L$800

; capture game blocks $03..$05
*811:03 N 800G
*BSAVE BLOCK.03,A$8E00,L$800
*811:04 N 800G
*BSAVE BLOCK.04,A$8E00,L$800
*811:05 N 800G
*BSAVE BLOCK.05,A$8E00,L$800

; capture game blocks $06..$13
*811:06 N 800G
*BSAVE BLOCK.06,A$9600,L$C00
*811:07 N 800G
*BSAVE BLOCK.07,A$9600,L$C00
*811:08 N 800G
*BSAVE BLOCK.08,A$9600,L$1800
*811:09 N 800G
*BSAVE BLOCK.09,A$9600,L$C00
*811:0A N 800G
*BSAVE BLOCK.0A,A$9600,L$1800
*811:0B N 800G
*BSAVE BLOCK.0B,A$9600,L$1800
*811:0C N 800G
*BSAVE BLOCK.0C,A$9600,L$C00
*811:0D N 800G
*BSAVE BLOCK.0D,A$9600,L$C00
*811:0E N 800G
*BSAVE BLOCK.0E,A$9600,L$C00
*811:0F N 800G
*BSAVE BLOCK.0F,A$9600,L$C00
*811:10 N 800G
*BSAVE BLOCK.10,A$9600,L$1800
*811:11 N 800G
*BSAVE BLOCK.11,A$9600,L$1800
*811:12 N 800G
*BSAVE BLOCK.12,A$9600,L$1800
*811:13 N 800G
*BSAVE BLOCK.13,A$9600,L$C00

*CATALOG

C1983 DSR^C#254
015 FREE

 A 002 HELLO
 B 024 ADVANCED DEMUFFIN 1.5
 B 013 _4LIVE
 S 005 _4LIVE DATA
*B 003 TRACE
 B 003 BOOT.0800-08FF
*B 003 TRACE2
 B 003 BOOT.0300-03FF
*B 003 TRACE3
 B 003 BOOT.0100-01FF
*B 003 TRACE4
 B 006 BOOT.0400-07FF
 B 003 TRACE5
 B 033 OBJ.0800-26FF
 B 048 OBJ.6000-8DFF
 B 009 OBJ.B800-BEFF
 B 002 READBLOCK
 B 009 OBJ.B800-BEFF
 B 002 READBLOCK
 B 010 BLOCK.00
 B 010 BLOCK.01
 B 010 BLOCK.02
 B 010 BLOCK.03
 B 010 BLOCK.04
 B 010 BLOCK.05
 B 003 OBJ.BF00-BFFF
 B 014 BLOCK.06
 B 014 BLOCK.07
 B 026 BLOCK.08
 B 014 BLOCK.09
 B 026 BLOCK.0A
 B 026 BLOCK.0B
 B 014 BLOCK.0C
 B 014 BLOCK.0D
 B 014 BLOCK.0E
 B 014 BLOCK.0F
 B 026 BLOCK.10
 B 026 BLOCK.11
 B 026 BLOCK.12
 B 014 BLOCK.13

It's... it's beautiful.  *wipes tear*

We have no further use for the original
disk. Now would be an excellent time to
take it out of the drive and store it
in a cool, dry place.

                   ~

              Chapter 13
      What's Our Vector, Victor?


What about the other vector, at $BFFD?
Did we ever look into that? Let's look
into that. It jumps to $BB00.

*BB00L

; turn on drive motor and wait
BB00-   AE F1 BF    LDX   $BFF1
BB03-   BD 89 C0    LDA   $C089,X
BB06-   A9 00       LDA   #$00
BB08-   20 A8 FC    JSR   $FCA8

; seek to track $22.5
BB0B-   A9 45       LDA   #$45
BB0D-   20 00 BE    JSR   $BE00

OK I just figured it out. This must be
the routine that writes out updated
high scores to disk. We saw earlier
that these are read from track $22.5
during boot. If the first vector was
for reading, I bet this vector is for
writing the only thing the game ever
writes: high scores.

; lots of standard how-to-write-to-a-
; floppy-disk code
BB10-   A0 FF       LDY   #$FF
BB12-   BD 8D C0    LDA   $C08D,X
BB15-   BD 8E C0    LDA   $C08E,X
BB18-   9D 8F C0    STA   $C08F,X
BB1B-   1D 8C C0    ORA   $C08C,X
BB1E-   A9 80       LDA   #$80
BB20-   20 A8 FC    JSR   $FCA8
BB23-   20 A8 FC    JSR   $FCA8
BB26-   BD 8D C0    LDA   $C08D,X
BB29-   BD 8E C0    LDA   $C08E,X
BB2C-   98          TYA
BB2D-   9D 8F C0    STA   $C08F,X
BB30-   1D 8C C0    ORA   $C08C,X
BB33-   48          PHA
BB34-   68          PLA

; write prologue nibbles
BB35-   20 57 FF    JSR   $FF57
BB38-   C8          INY
BB39-   9D 8D C0    STA   $C08D,X
BB3C-   1D 8C C0    ORA   $C08C,X
BB3F-   B9 83 BB    LDA   $BB83,Y
BB42-   D0 F1       BNE   $BB35

*BB83.

BB83-          FF 3F CF F3 FC
BB88- FF 3F CF F3 FC FF D5 FF
BB90- DD 00

The first few nibbles are to sync the
drive to the start of an 8-bit nibble.
The last three, $D5 $FF $DD, match the
game-specific nibble sequence that the
read routine matched during boot (at
$0460).

; reset Y to 0
BB44-   A8          TAY
BB45-   EA          NOP
BB46-   EA          NOP

; always take bytes from $B400
BB47-   B9 00 B4    LDA   $B400,Y

; convert to nibbles
BB4A-   48          PHA
BB4B-   4A          LSR
BB4C-   09 AA       ORA   #$AA
BB4E-   9D 8D C0    STA   $C08D,X
BB51-   DD 8C C0    CMP   $C08C,X
BB54-   C1 00       CMP   ($00,X)
BB56-   EA          NOP
BB57-   EA          NOP
BB58-   48          PHA
BB59-   68          PLA
BB5A-   68          PLA
BB5B-   09 AA       ORA   #$AA
BB5D-   9D 8D C0    STA   $C08D,X
BB60-   DD 8C C0    CMP   $C08C,X
BB63-   48          PHA
BB64-   68          PLA

; write $100 bytes
BB65-   C8          INY
BB66-   D0 DF       BNE   $BB47

; write a 1-nibble epilogue
BB68-   A9 D5       LDA   #$D5
BB6A-   C1 00       CMP   ($00,X)
BB6C-   EA          NOP
BB6D-   EA          NOP
BB6E-   9D 8D C0    STA   $C08D,X
BB71-   1D 8C C0    ORA   $C08C,X

; wait to ensure the epilogue is
; written, then turn off drive and exit
BB74-   A9 08       LDA   #$08
BB76-   20 A8 FC    JSR   $FCA8
BB79-   BD 8E C0    LDA   $C08E,X
BB7C-   BD 8C C0    LDA   $C08C,X
BB7F-   BD 88 C0    LDA   $C088,X
BB82-   60          RTS

And that's all she wrote.

One thing I want to do with this
information is save the high scores
data as its own file. The default
scores are read into $2600, which I've
captured.

*BLOAD OBJ.0800-26FF,A$800
*BSAVE SCORES,A$2600,L$100

                   ~

              Chapter 14
      A Man, A Plan, A Canal, &c.


Let's step back from the low-level code
for a moment and talk about how this
game interacts with the disk at a high
level.

- There is no runtime protection check.
  All the "protection" is structural,
  to make the disk difficult to copy
  with a bit copier. Data is stored on
  whole tracks, half tracks, even some
  consecutive half tracks. But there
  are no nibble checks or secondary
  protections, even as it's loading
  additional level-specific data
  throughout the game.

- The game code itself contains no disk
  code. It's completely isolated. The
  game communicates with the RWTS
  through vectors at $BFFA and $BFFD.

- The bootloader code at $0400..$07FF
  is not used after boot; the entire
  text page can get cleared in several
  cases, such as viewing high scores.

- Game code lives at $0300..$038F,
  $0800..$1FFF, and $6000..$ADFF, plus
  one page at $B400 for high scores.
  All memory above $B500 is available.

This is great news. It gives us total
flexibility to recreate the game from
its constituent pieces.

Here's the plan:

1. Write all the game code and data to
   a standard 16-sector disk

2. Write a bootloader and RWTS that can
   read the main game code into memory

3. Write some glue code to mimic the
   vectors at $BFFA (to dynamically
   load blocks from disk) and $BFFD (to
   write out high scores). If I do it
   properly, all the existing game code
   will "just work" and never know the
   difference.

4. Declare victory (*)

(*) take a nap

The original disk used a fairly space-
inefficient 4-and-4 encoding to store
data on disk. With a standard 6-and-2
encoding, we'll have plenty of room to,
um, spare. Each game block can start on
its own track, which saves a few bytes
by being able to hard-code the starting
sector (0) for each block. Many tracks
won't use all 16 sectors.

The disk map will look like this
(tr = track, sc = sector count, all
numbers in hexadecimal):

tr | sc | memory     | notes
---+----+------------+-----------------
00 | 03 | BD00..BFFF | Spareboot
01 | 10 | 0800..17FF | game code
02 | 0F | 1800..26FF |   (cont.)
03 | 10 | 6000..6FFF |   (cont.)
04 | 10 | 7000..7FFF |   (cont.)
05 | 0E | 8000..8DFF |   (cont.)
06 |    |            | unused
07 |    |            | unused
08 | 08 | 8E00..95FF | block 00
09 | 08 | 8E00..95FF | block 01
0A | 08 | 8E00..95FF | block 02
0B | 08 | 8E00..95FF | block 03
0C | 08 | 8E00..95FF | block 04
0D | 08 | 8E00..95FF | block 05
0E | 0C | 9600..A1FF | block 06
0F | 0C | 9600..A1FF | block 07
10 | 10 | 9600..A5FF | block 08
11 | 08 | A600..ADFF |   (cont.)
12 | 0C | 9600..A1FF | block 09
13 | 10 | 9600..A5FF | block 0A
14 | 08 | A600..ADFF |   (cont.)
15 | 10 | 9600..A5FF | block 0B
16 | 08 | A600..ADFF |   (cont.)
17 | 0C | 9600..A1FF | block 0C
18 | 0C | 9600..A1FF | block 0D
19 | 0C | 9600..A1FF | block 0E
1A | 0C | 9600..A1FF | block 0F
1B | 10 | 9600..A5FF | block 10
1C | 08 | A600..ADFF |   (cont.)
1D | 10 | 9600..A5FF | block 11
1E | 08 | A600..ADFF |   (cont.)
1F | 10 | 9600..A5FF | block 12
20 | 08 | A600..ADFF |   (cont.)
21 | 0C | 9600..A1FF | block 13
22 | 01 | B400..B4FF | high scores

I wrote a build script to take all the
chunks of game code I captured way back
in chapter 12. And by "script," I mean
"BASIC program."

]PR#5
...

 10  REM  MAKE SPARE CHANGE
 11  REM  S6,D1=BLANK DISK
 12  REM  S5,D1=WORK DISK
 20 D$ =  CHR$ (4)

Load the game code and write it to
tracks $01..$05:

 30  PRINT D$"BLOAD OBJ.0800-26FF,
     A$2000"
 40 COUNT = 31:TRK = 1: GOSUB 1000
 50  PRINT D$"BLOAD OBJ.6000-8DFF,
     A$2000"
 60 COUNT = 46:TRK = 3: GOSUB 1000

Load block 0 and write it to track $08:

 70 HGR : TEXT : PRINT D$"BLOAD
    BLOCK.00,A$2000"
 80 COUNT = 8:TRK = 8: GOSUB 1000

Load block 1 and write it to track $09:

 90  PRINT D$"BLOAD BLOCK.01,A$2000"
 100 TRK = 9: GOSUB 1000

And so on:

 110  PRINT D$"BLOAD BLOCK.02,A$2000"
 120 TRK = 10: GOSUB 1000
 130  PRINT D$"BLOAD BLOCK.03,A$2000"
 140 TRK = 11: GOSUB 1000
 150  PRINT D$"BLOAD BLOCK.04,A$2000"
 160 TRK = 12: GOSUB 1000
 170  PRINT D$"BLOAD BLOCK.05,A$2000"
 180 TRK = 13: GOSUB 1000
 190  PRINT D$"BLOAD BLOCK.06,A$2000"
 200 TRK = 14: GOSUB 1000
 210  PRINT D$"BLOAD BLOCK.07,A$2000"
 220 TRK = 15: GOSUB 1000

Note that some blocks take two tracks:

 230  PRINT D$"BLOAD BLOCK.08,A$2000"
 240 TRK = 16:COUNT = 24: GOSUB 1000
 250  PRINT D$"BLOAD BLOCK.09,A$2000"
 260 TRK = 18:COUNT = 12: GOSUB 1000
 270  PRINT D$"BLOAD BLOCK.0A,A$2000"
 280 TRK = 19:COUNT = 24: GOSUB 1000
 290  PRINT D$"BLOAD BLOCK.0B,A$2000"
 300 TRK = 21: GOSUB 1000
 310  PRINT D$"BLOAD BLOCK.0C,A$2000"
 320 TRK = 23:COUNT = 12: GOSUB 1000
 330  PRINT D$"BLOAD BLOCK.0D,A$2000"
 340 TRK = 24: GOSUB 1000
 350  PRINT D$"BLOAD BLOCK.0E,A$2000"
 360 TRK = 25: GOSUB 1000
 370  PRINT D$"BLOAD BLOCK.0F,A$2000"
 380 TRK = 26: GOSUB 1000
 390  PRINT D$"BLOAD BLOCK.10,A$2000"
 400 TRK = 27:COUNT = 24: GOSUB 1000
 410  PRINT D$"BLOAD BLOCK.11,A$2000"
 420 TRK = 29: GOSUB 1000
 430  PRINT D$"BLOAD BLOCK.12,A$2000"
 440 TRK = 31: GOSUB 1000
 450  PRINT D$"BLOAD BLOCK.13,A$2000"
 460 TRK = 33:COUNT = 12: GOSUB 1000

Finally, the scores go on track $22:

 470  PRINT D$"BLOAD SCORES,A$2000"
 480 TRK = 34:COUNT = 1: GOSUB 1000
 999  END
 1000  REM  WRITE TO DISK
 1010  PRINT D$"BLOAD WRITE,A$BE00"
 1020  POKE 48780,TRK
 1030  POKE 48781,0: REM  SECTOR
 1040  POKE 48785,32: REM  PAGE
 1050  POKE 48641,COUNT
 1060  CALL 48640
 1070  RETURN

]SAVE MAKE

The BASIC program relies on a short
assembly language routine to do the
actual writing to disk. Here is that
routine (reloaded on line 1010 every
time we GOSUB 1000):

]CALL -151

; page count at $BE01 (set from BASIC)
BE00-   A9 D1       LDA   #$D1
BE02-   85 FF       STA   $FF

; logical sector (incremented)
BE04-   A9 00       LDA   #$00
BE06-   85 FE       STA   $FE

; call RWTS to write sector
BE08-   A9 BE       LDA   #$BE
BE0A-   A0 88       LDY   #$88
BE0C-   20 D9 03    JSR   $03D9

; increment logical sector, wrap around
; from $0F to $00 and increment track
BE0F-   E6 FE       INC   $FE
BE11-   A4 FE       LDY   $FE
BE13-   C0 10       CPY   #$10
BE15-   D0 07       BNE   $BE1E
BE17-   A0 00       LDY   #$00
BE19-   84 FE       STY   $FE
BE1B-   EE 8C BE    INC   $BE8C

; convert logical to physical sector
BE1E-   B9 40 BE    LDA   $BE40,Y
BE21-   8D 8D BE    STA   $BE8D

; increment page to write
BE24-   EE 91 BE    INC   $BE91

; loop until done with all sectors
BE27-   C6 FF       DEC   $FF
BE29-   D0 DD       BNE   $BE08
BE2B-   60          RTS

*BE40.BE4F

; logical to physical sector mapping
BE40- 00 07 0E 06 0D 05 0C 04
BE48- 0B 03 0A 02 09 01 08 0F

*BE88.BE97

; RWTS parameter table, pre-initialized
; with slot (#$06), drive (#$01), and
; RWTS write command (#$02)
BE88- 01 60 01 00 D1 00 FB F7
                  ^^
                 track (set from BASIC)

BE90- 00 D1 00 00 02 00 00 60
         ^^
      address (set from BASIC)

*BSAVE WRITE,A$BE00,L$98

[S6,D1=blank disk]

]RUN MAKE
...write write write...

Boom! The entire game is written out to
a standard 16-sector disk.

Now we get to build an RWTS that can
read it.

                   ~

              Chapter 15
         Introducing Spareboot


Spareboot is a fast bootloader and full
read/write RWTS. It fits on track $00
with plenty of room to, um, spare. It
uses only 6 pages of memory for all its
code + data + scratch space. It uses no
zero page addresses after boot. It can
read an entire track in one revolution.

qkumba wrote it from scratch. I mostly
just cheered.

After initialization, Spareboot is dead
simple and always ready to use:

entry | command     | parameters
------+-------------+------------------
$BD00 | read_block  | A = game block ID
------+-------------+------------------
$BE00 | write_score | none
------+-------------+------------------
$BF00 | seek        | none

Some important notes:

- The read_block routine always reads
  consecutive tracks in physical sector
  order into consecutive pages in
  memory. There is no translation from
  physical to logical sectors.

- The write_score routine hard-codes
  the only thing we ever write: the
  high scores from $B400 onto track $22
  sector $00. The low-level twiddling
  is adapted directly from DOS 3.3, and
  I will not go into it further.

- The seek routine can seek forward or
  back to any whole track. (I mention
  this because some fastloaders can
  only seek forward.) It is called by
  the other two routines. I will not go
  into it further either.

- This is the same API as the disk
  subsystem provides to the game on the
  original disk. The vector at $BFFA
  can jump to this read_block routine,
  the vector at $BFFD can jump to this
  write_score routine, and we won't
  need to change a single byte of code
  inside the game itself. It literally
  will never know the difference.

I said Spareboot takes 6 pages in
memory, but I've only mentioned 3. The
other 3 are for data:

$BA00..$BAFF - scratch space for write
$BB00..$BB95 - data tables for write
$BB96..$BCFF - data tables for read

Spareboot starts, as all disks start,
on track $00. The drive controller ROM
reads sector $00 into $0800 and jumps
to $0801. Our code at $0801 reuses the
the disk controller ROM to read three
additional sectors into $BC00..$BFFF.
Then we create a few data tables, self-
modify to accommodate booting from any
slot, and set up a stack frame to load
the rest of the game.

; tell the ROM to load only this sector
; (we'll do the rest manually)
0800-  [01]

; The accumulator is #$01 after loading
; sector $00, #$03 after loading sector
; $0E, #$05 after loading sector $0D,
; and #$07 after loading sector $0C.
; We shift it right to divide by 2,
; then use that to calculate the load
; address of the next sector.
0801-   4A          LSR

; Sector $0E => $BD00
; Sector $0D => $BE00
; Sector $0C => $BF00
0802-   69 BC       ADC   #$BC

; store the load address
0804-   85 27       STA   $27

; shift the accumulator again (now that
; we've stored the load address)
0806-   0A          ASL
0807-   0A          ASL

; transfer X (boot slot x16) to the
; accumulator, which will be useful
; later but doesn't affect the carry
; flag we may have just tripped with
; the two "ASL" instructions
0808-   8A          TXA

; if the two "ASL" instructions set the
; carry flag, it means the load address
; was at least #$C0, which means we've
; loaded all the sectors we wanted to
; load and we should exit this loop
0809-   B0 25       BCS   $0830

; Set up next sector number to read.
; The disk controller ROM does this
; once already, but due to quirks of
; timing, it's much faster to increment
; it twice so the next sector you want
; to load is actually the next sector
; under the drive head. Otherwise you
; end up waiting for the disk to spin
; an entire revolution, which is quite
; slow.
080B-   E6 3D       INC   $3D

; Set up the "return" address to jump
; to the "read sector" entry point of
; the disk controller ROM. This could
; be anywhere in $Cx00 depending on the
; slot we booted from, which is why we
; put the boot slot in the accumulator
; at $0808.
080D-   4A          LSR
080E-   4A          LSR
080F-   4A          LSR
0810-   4A          LSR
0811-   09 C0       ORA   #$C0

; push the entry point on the stack
0813-   48          PHA
0814-   A9 5B       LDA   #$5B
0816-   48          PHA

; "Return" to the entry point via RTS.
; The disk controller ROM always jumps
; to $0801 (that's why we moved it and
; patched it to trace the boot), so
; this entire thing is a loop that only
; exits via the "BCS" branch at $0809.
0817-   60          RTS

; Execution continues here (from $0809)
; after three sectors have been loaded
; into memory at $BD00..$BFFF.
; There are a number of places in boot1
; that hit a slot-specific soft switch
; (read a nibble from disk, turn off
; the drive, &c). Rather than the usual
; form of "LDA $C08C,X", we will use
; "LDA $C0EC" and modify the $EC byte
; in advance, based on the boot slot.
; $0820 is an array of all the places
; in the Spareboot code that get this
; adjustment.
0830-   09 8C       ORA   #$8C
0832-   A2 00       LDX   #$00
0834-   BC 20 08    LDY   $0820,X
0837-   84 26       STY   $26
0839-   BC 21 08    LDY   $0821,X
083C-   F0 0A       BEQ   $0848
083E-   84 27       STY   $27
0840-   A0 00       LDY   #$00
0842-   91 26       STA   ($26),Y
0844-   E8          INX
0845-   E8          INX
0846-   D0 EC       BNE   $0834

; munge $EC -> $E8 (used later to turn
; off the drive motor)
0848-   29 F8       AND   #$F8
084A-   8D FC BD    STA   $BDFC

; munge $E8 -> $E9 (used later to turn
; on the drive motor)
084D-   09 01       ORA   #$01
084F-   8D 66 BF    STA   $BF66
0852-   8D 09 BE    STA   $BE09

; munge $E9 -> $E0 (used later to move
; the drive head via the stepper motor)
0855-   49 09       EOR   #$09
0857-   8D 50 BF    STA   $BF50

; munge $E0 -> $60 (boot slot x16, used
; during seek and write routines)
085A-   29 70       AND   #$70
085C-   8D 39 BE    STA   $BE39
085F-   8D 6B BE    STA   $BE6B
0862-   8D 81 BE    STA   $BE81
0865-   8D AE BE    STA   $BEAE

                   ~

              Chapter 16
                 6 + 2


Before I dive into the next chunk of
code, I get to pause and explain a
little bit of theory. As you probably
know if you're the sort of person who's
read this far already, Apple II floppy
disks do not contain the actual data
that ends up being loaded into memory.
Due to hardware limitations of the
original Disk II drive, data on disk is
stored in an intermediate format called
"nibbles." Bytes in memory are encoded
into nibbles before writing to disk,
and nibbles that you read from the disk
must be decoded back into bytes. The
round trip is lossless but requires
some bit wrangling.

Decoding nibbles-on-disk into bytes-in-
memory is a multi-step process. In
"6-and-2 encoding" (used by DOS 3.3,
ProDOS, and all ".dsk" image files),
there are 64 possible values that you
may find in the data field (in the
range $96..$FF, but not all of those,
because some of them have bit patterns
that trip up the drive firmware). We'll
call these "raw nibbles."

Step 1: read $156 raw nibbles from the
data field. These values will range
from $96 to $FF, but as mentioned
earlier, not all values in that range
will appear on disk.

Now we have $156 raw nibbles.

Step 2: decode each of the raw nibbles
into a 6-bit byte between 0 and 63
(%00000000 and %00111111 in binary).
$96 is the lowest valid raw nibble, so
it gets decoded to 0. $97 is the next
valid raw nibble, so it's decoded to 1.
$98 and $99 are invalid, so we skip
them, and $9A gets decoded to 2. And so
on, up to $FF (the highest valid raw
nibble), which gets decoded to 63.

Now we have $156 6-bit bytes.

Step 3: split up each of the first $56
6-bit bytes into pairs of bits. In
other words, each 6-bit byte becomes
three 2-bit bytes. These 2-bit bytes
are merged with the next $100 6-bit
bytes to create $100 8-bit bytes. Hence
the name, "6-and-2" encoding.

The exact process of how the bits are
split and merged is... complicated. The
first $56 6-bit bytes get split up into
2-bit bytes, but those two bits get
swapped (so %01 becomes %10 and vice-
versa). The other $100 6-bit bytes each
get multiplied by 4 (a.k.a. bit-shifted
two places left). This leaves a hole in
the lower two bits, which is filled by
one of the 2-bit bytes from the first
group.

A diagram might help. "a" through "x"
each represent one bit.

             -------------

1 decoded      3 decoded
nibble in  +   nibbles in   =  3 bytes
first $56      other $100


00abcdef       00ghijkl
               00mnopqr
   |           00stuvwx
   |
 split            |
   &           shifted
swapped        left x2
   |              |
   V              V

000000fe   +   ghijkl00   =   ghijklfe
000000dc   +   mnopqr00   =   mnopqrdc
000000ba   +   stuvwx00   =   stuvwxba

             -------------

Tada! Four 6-bit bytes

  00abcdef
  00ghijkl
  00mnopqr
  00stuvwx

become three 8-bit bytes

  ghijklfe
  mnopqrdc
  stuvwxba

When DOS 3.3 reads a sector, it reads
the first $56 raw nibbles, decoded them
into 6-bit bytes, and stashes them in a
temporary buffer (at $BC00). Then it
reads the other $100 raw nibbles,
decodes them into 6-bit bytes, and puts
them in another temporary buffer (at
$BB00). Only then does DOS 3.3 start
combining the bits from each group to
create the full 8-bit bytes that will
end up in the target page in memory.
This is why DOS 3.3 "misses" sectors
when it's reading, because it's busy
twiddling bits while the disk is still
spinning.

Spareboot also uses "6-and-2" encoding.
The first $56 nibbles in the data field
are still split into pairs of bits that
will be merged with nibbles that won't
come until later. But instead of
waiting for all $156 raw nibbles to be
read from disk, it "interleaves" the
nibble reads with the bit twiddling
required to merge the first $56 6-bit
bytes and the $100 that follow. By the
time Spareboot gets to the data field
checksum, it has already stored all
$100 8-bit bytes in their final resting
place in memory. This means that we can
read all 16 sectors on a track in one
revolution of the disk. That's what
makes it crazy fast.

To make it possible to twiddle the bits
and not miss nibbles as the disk
spins(*), we do some of the work in
advance. We multiply each of the 64
possible decoded values by 4 and store
those values. (Since this is done by
bit shifting and we're doing it before
we start reading the disk, this is
called the "pre-shift" table.) We also
store all possible 2-bit values in a
repeating pattern that will make it
easy to look them up later. Then, as
we're reading from disk (and timing is
tight), we can simulate bit math with a
series of table lookups. There is just
enough time to convert each raw nibble
into its final 8-bit byte before
reading the next nibble.

(*) The disk spins independently of the
    CPU, and we only have a limited
    time to read a nibble and do what
    we're going to do with it before
    WHOOPS HERE COMES ANOTHER ONE. So
    time is of the essence. Also, "As
    The Disk Spins" would make a great
    name for a retrocomputing-themed
    soap opera.

The first table, at $BC00..$BCFF, is
three columns wide and 64 rows deep.
Astute readers will notice that 3 x 64
is not 256. Only three of the columns
are used; the fourth (unused) column
exists because multiplying by 3 is hard
but multiplying by 4 is easy (in base 2
anyway). The three columns correspond
to the three pairs of 2-bit values in
those first $56 6-bit bytes. Since the
values are only 2 bits wide, each
column holds one of four different
values (%00, %01, %10, or %11).

The second table, at $BB96..$BBFF, is
the "pre-shift" table. This contains
all the possible 6-bit bytes, in order,
each multiplied by 4 (a.k.a. shifted to
the left two places, so the 6 bits that
started in columns 0-5 are now in
columns 2-7, and columns 0 and 1 are
zeroes). Like this:

       00ghijkl   -->   ghijkl00

Astute readers will notice that there
are only 64 possible 6-bit bytes, but
this second table is larger than 64
bytes. To make lookups easier, the
table has empty slots for each of the
invalid raw nibbles. In other words, we
don't do any math to decode raw nibbles
into 6-bit bytes; we just look them up
in this table (offset by $96, since
that's the lowest valid raw nibble) and
get the required bit shifting for free.


 addr | raw | decoded 6-bit | pre-shift
------+-----+---------------+----------
$BB96 | $96 | 0 = %00000000 | %00000000
$BB97 | $97 | 1 = %00000001 | %00000100
$BB98 | $98       [invalid raw nibble]
$BB99 | $99       [invalid raw nibble]
$BB9A | $9A | 2 = %00000010 | %00001000
$BB9B | $9B | 3 = %00000011 | %00001100
$BB9C | $9C       [invalid raw nibble]
$BB9D | $9D | 4 = %00000100 | %00010000
  .
  .
  .
$BBFE | $FE | 62 = %00111110 | %11111000
$BBFF | $FF | 63 = %00111111 | %11111100


Each value in this "pre-shift" table
also serves as an index into the first
table (with all the 2-bit bytes). This
wasn't an accident; I mean, that sort
of magic doesn't just happen. But the
table of 2-bit bytes is arranged in
such a way that we can take one of the
raw nibbles to be decoded and split
apart (from the first $56 raw nibbles
in the data field), use each raw nibble
as an index into the pre-shift table,
then use that pre-shifted value as an
index into the first table to get the
2-bit value we need.

                   ~

              Chapter 17
           Back to Spareboot


This is the loop that creates the
pre-shift table at $BB96. As a special
bonus, it also creates the inverse
table that is used during disk write
operations (converting in the other
direction).

0868-   A2 3F       LDX   #$3F
086A-   86 FF       STX   $FF
086C-   E8          INX
086D-   A0 7F       LDY   #$7F
086F-   84 FE       STY   $FE
0871-   98          TYA
0872-   0A          ASL
0873-   24 FE       BIT   $FE
0875-   F0 18       BEQ   $088F
0877-   05 FE       ORA   $FE
0879-   49 FF       EOR   #$FF
087B-   29 7E       AND   #$7E
087D-   B0 10       BCS   $088F
087F-   4A          LSR
0880-   D0 FB       BNE   $087D
0882-   CA          DEX
0883-   8A          TXA
0884-   0A          ASL
0885-   0A          ASL
0886-   99 80 BB    STA   $BB80,Y
0889-   98          TYA
088A-   09 80       ORA   #$80
088C-   9D 56 BB    STA   $BB56,X
088F-   88          DEY
0890-   D0 DD       BNE   $086F

And this is the result (".." means the
address is uninitialized and unused):

BB90-                   00 04
BB98- .. .. 08 0C .. 10 14 18
BBA0- .. .. .. .. .. .. 1C 20
BBA8- .. .. .. 24 28 2C 30 34
BBB0- .. .. 38 3C 40 44 48 4C
BBB8- .. 50 54 58 5C 60 64 68
BBC0- .. .. .. .. .. .. .. ..
BBC8- .. .. .. 6C .. 70 74 78
BBD0- .. .. .. 7C .. .. 80 84
BBD8- .. 88 8C 90 94 98 9C A0
BBE0- .. .. .. .. .. A4 A8 AC
BBE8- .. B0 B4 B8 BC C0 C4 C8
BBF0- .. .. CC D0 D4 D8 DC E0
BBF8- .. E4 E8 EC F0 F4 F8 FC

Next up: a loop to create the table of
2-bit values at $BC00, magically
arranged to enable easy lookups later.

0892-   84 FD       STY   $FD
0894-   46 FF       LSR   $FF
0896-   46 FF       LSR   $FF
0898-   BD 2C 08    LDA   $082C,X
089B-   99 00 BC    STA   $BC00,Y
089E-   E6 FD       INC   $FD
08A0-   A5 FD       LDA   $FD
08A2-   25 FF       AND   $FF
08A4-   D0 05       BNE   $08AB
08A6-   E8          INX
08A7-   8A          TXA
08A8-   29 03       AND   #$03
08AA-   AA          TAX
08AB-   C8          INY
08AC-   C8          INY
08AD-   C8          INY
08AE-   C8          INY
08AF-   C0 03       CPY   #$03
08B1-   B0 E5       BCS   $0898
08B3-   C8          INY
08B4-   C0 03       CPY   #$03
08B6-   90 DC       BCC   $0894

And this is the result:

BC00- 00 00 00 .. 00 00 02 ..
BC08- 00 00 01 .. 00 00 03 ..
BC10- 00 02 00 .. 00 02 02 ..
BC18- 00 02 01 .. 00 02 03 ..
BC20- 00 01 00 .. 00 01 02 ..
BC28- 00 01 01 .. 00 01 03 ..
BC30- 00 03 00 .. 00 03 02 ..
BC38- 00 03 01 .. 00 03 03 ..
BC40- 02 00 00 .. 02 00 02 ..
BC48- 02 00 01 .. 02 00 03 ..
BC50- 02 02 00 .. 02 02 02 ..
BC58- 02 02 01 .. 02 02 03 ..
BC60- 02 01 00 .. 02 01 02 ..
BC68- 02 01 01 .. 02 01 03 ..
BC70- 02 03 00 .. 02 03 02 ..
BC78- 02 03 01 .. 02 03 03 ..
BC80- 01 00 00 .. 01 00 02 ..
BC88- 01 00 01 .. 01 00 03 ..
BC90- 01 02 00 .. 01 02 02 ..
BC98- 01 02 01 .. 01 02 03 ..
BCA0- 01 01 00 .. 01 01 02 ..
BCA8- 01 01 01 .. 01 01 03 ..
BCB0- 01 03 00 .. 01 03 02 ..
BCB8- 01 03 01 .. 01 03 03 ..
BCC0- 03 00 00 .. 03 00 02 ..
BCC8- 03 00 01 .. 03 00 03 ..
BCD0- 03 02 00 .. 03 02 02 ..
BCD8- 03 02 01 .. 03 02 03 ..
BCE0- 03 01 00 .. 03 01 02 ..
BCE8- 03 01 01 .. 03 01 03 ..
BCF0- 03 03 00 .. 03 03 02 ..
BCF8- 03 03 01 .. 03 03 03 ..

And with that, Spareboot is fully armed
and operational.

                   ~

              Chapter 18
            Read & Go Seek


In a standard DOS 3.3 RWTS, the
softswitch to read the data latch is
"LDA $C08C,X", where X is the boot slot
times 16 (to allow disks to boot from
any slot). Spareboot also supports
booting and reading from any slot, but
instead of using an index, most fetch
instructions are set up in advance
based on the boot slot. Not only does
this free up the X register, it lets us
juggle all the registers and put the
raw nibble value in whichever one is
convenient at the time. (We take full
advantage of this freedom.) I've marked
each pre-set softswitch with "o_O".

There are several other instances of
addresses and constants that get
modified while Spareboot is executing.
I've left these with a bogus value $D1
and marked them with "o_O".

Spareboot's source code should be
available from the same place you found
this write-up. If you're looking to
modify this code for your own purposes,
I suggest you "use the source, Luke."

; $BD00 is the entry point to read a
; game block by ID number. This was at
; $BC00 on the original disk.
; A = ID number on entry
BD00-   A8          TAY

; get the starting address (high byte)
; of this game block, and store it
BD01-   B9 A5 BF    LDA   $BFA5,Y
BD04-   8D 24 BD    STA   $BD24

; get the length (number of sectors) of
; this game block, and store it
BD07-   BE 8E BF    LDX   $BF8E,Y
BD0A-   8E EF BD    STX   $BDEF

; get the starting track of this game
; block
BD0D-   B9 77 BF    LDA   $BF77,Y
BD10-   20 61 BF    JSR   $BF61

*BF61L

; multiple track number by 2 to convert
; to a phase, and store that
BF61-   0A          ASL
BF62-   8D 0C BF    STA   $BF0C

; turn on drive motor
BF65-   AD E9 C0    LDA   $C0E9     o_O

; To ensure the drive is fully spun up,
; poll for good data (an $FF nibble
; followed by a non-$FF nibble) before
; continuing. The subroutine at $BEE6
; reads a single nibble (not shown)
BF68-   20 E6 BE    JSR   $BEE6
BF6B-   C9 FF       CMP   #$FF
BF6D-   D0 F9       BNE   $BF68
BF6F-   20 E6 BE    JSR   $BEE6
BF72-   C9 FF       CMP   #$FF
BF74-   F0 F9       BEQ   $BF6F
BF76-   60          RTS

Here are the tables for the starting
track, sector count, and starting
address, at $BF77, $BF8E, and $BFA5
respectively:

BF77-                      08
BF78- 09 0A 0B 0C 0D 0E 0F 10
BF80- 12 13 15 17 18 19 1A 1B
BF88- 1D 1F 21 01 03 22

BF8E-                   08 08
BF90- 08 08 08 08 0C 0C 18 0C
BF98- 18 18 0C 0C 0C 0C 18 18
BFA0- 18 0C 1F 2E 01

BFA5-                8E 8E 8E
BFA8- 8E 8E 8E 96 96 96 96 96
BFB0- 96 96 96 96 96 96 96 96
BFB8- 96 08 60 26

The track values match the disk map I
made in chapter 14. The addresses and
sector counts match the game blocks
defined by the original disk.

Continuing from $BD13...

; are we reading this entire track?
BD13-   A9 10       LDA   #$10
BD15-   CD EF BD    CMP   $BDEF

; no
BD18-   B0 01       BCS   $BD1B

; yes
BD1A-   AA          TAX

; either way, X is now the number of
; sectors we want to read from this
; specific track (can be anything from
; #$01 to #$10)
BD1B-   8E 22 BD    STX   $BD22

; seek to the track we want (not shown,
; but it goes to the track we passed
; into $BF61 earlier)
BD1E-   20 00 BF    JSR   $BF00

; Initialize an array of which sectors
; we've read from the current track.
; The array is in physical sector
; order, thus the RWTS assumes data is
; stored in physical sector order on
; each track. (This saves 18 bytes: 16
; for the table and 2 for the lookup
; command!) Values are the actual pages
; in memory where that sector should
; go, and they get zeroed once the
; sector is read (so we don't waste
; time decoding the same sector twice).
; $BD22 (sectors on this track) was set
; at $BD1B.
; $BD24 (starting address, high byte)
; was set at $BD04.
; Y is always #$00 coming out of the
; seek subroutine. We'll reuse it as
; an index into the array. This means
; every track read starts on sector 0.
BD21-   A2 D1       LDX   #$D1      o_O
BD23-   A9 D1       LDA   #$D1      o_O
BD25-   99 BC BF    STA   $BFBC,Y
BD28-   EE 24 BD    INC   $BD24
BD2B-   C8          INY
BD2C-   CA          DEX
BD2D-   D0 F4       BNE   $BD23
BD2F-   20 D7 BE    JSR   $BED7

*BED7L

; This routine reads nibbles from disk
; until it finds the sequence "D5 AA",
; then it reads one more nibble and
; returns it in the accumulator. We
; reuse this routine to find both the
; address and data field prologues.
BED7-   20 E6 BE    JSR   $BEE6
BEDA-   C9 D5       CMP   #$D5
BEDC-   D0 F9       BNE   $BED7
BEDE-   20 E6 BE    JSR   $BEE6
BEE1-   C9 AA       CMP   #$AA
BEE3-   D0 F5       BNE   $BEDA
BEE5-   A8          TAY
BEE6-   AD EC C0    LDA   $C0EC     o_O
BEE9-   10 FB       BPL   $BEE6
BEEB-   60          RTS

Continuing from $BD32...

; If that third nibble is not #$AD, we
; assume it's the end of the address
; prologue. (#$96 would be the third
; nibble of a standard address
; prologue, but we don't actually
; check.) We fall through and start
; decoding the 4-4 encoded values in
; the address field.
BD32-   49 AD       EOR   #$AD
BD34-   F0 35       BEQ   $BD6B
BD36-   20 C4 BE    JSR   $BEC4

*BEC4L

; This routine parses the 4-4-encoded
; values in the address field. The
; first time through this loop, we'll
; read the disk volume number. The
; second time, we'll read the track
; number. The third time, we'll read
; the physical sector number. We don't
; actually care about the disk volume
; or the track number, and once we get
; the sector number, we don't verify
; the address field checksum.
BEC4-   A0 03       LDY   #$03
BEC6-   20 E6 BE    JSR   $BEE6
BEC9-   2A          ROL
BECA-   8D E0 BD    STA   $BDE0
BECD-   20 E6 BE    JSR   $BEE6
BED0-   2D E0 BD    AND   $BDE0
BED3-   88          DEY
BED4-   D0 F0       BNE   $BEC6

; On exit, the accumulator contains the
; physical sector number.
BED6-   60          RTS

Continuing from $BD39...

; use physical sector number as an
; index into the sector address array
BD39-   A8          TAY

; get the target page (where we want to
; store this sector in memory) from the
; array we initialized earlier at $BD21
BD3A-   BE BC BF    LDX   $BFBC,Y

; if the target page is #$00, it means
; we've already read this sector, so
; loop back to find the next address
; prologue
BD3D-   F0 F0       BEQ   $BD2F

; store the physical sector number
BD3F-   8D E0 BD    STA   $BDE0

; store the target page in several
; places throughout this routine
BD42-   8E 64 BD    STX   $BD64
BD45-   8E C4 BD    STX   $BDC4
BD48-   8E 7C BD    STX   $BD7C
BD4B-   8E 8E BD    STX   $BD8E
BD4E-   8E A6 BD    STX   $BDA6
BD51-   8E BE BD    STX   $BDBE
BD54-   E8          INX
BD55-   8E D9 BD    STX   $BDD9
BD58-   CA          DEX
BD59-   CA          DEX
BD5A-   8E 94 BD    STX   $BD94
BD5D-   8E AC BD    STX   $BDAC

; Save the two bytes immediately after
; the target page, because we're going
; to use them for temporary storage.
; We'll restore them later.
; This means that Spareboot can't be
; used to read sectors into $BF00,
; since $C000 and $C001 are read-only.
; But we'll never do that because
; Spareboot already lives there, so
; it's fine.
BD60-   A0 FE       LDY   #$FE
BD62-   B9 02 D1    LDA   $D102,Y
BD65-   48          PHA
BD66-   C8          INY
BD67-   D0 F9       BNE   $BD62

; This is an unconditional branch.
; (Figuring out why is left as a bonus
; exercise for the reader!)
BD69-   B0 C4       BCS   $BD2F

; execution continues here (from $BD34)
; after matching the data prologue
BD6B-   E0 00       CPX   #$00

; If X is still #$00, it means we found
; a data prologue before we found an
; address prologue. In that case, we
; have to skip this sector, because we
; don't know which sector it is and we
; wouldn't know where to put it. That's
; showbiz, baby!
BD6D-   F0 C0       BEQ   $BD2F

Nibble loop #1 reads nibbles $00..$55,
looks up the corresponding offset in
the preshift table at $BB96, and stores
that offset in the temporary two-byte
buffer after the target page.

; initialize rolling checksum to #$00,
; or update it with the results from
; the calculations below
BD6F-   8D 7E BD    STA   $BD7E

; read one nibble from disk
BD72-   AE EC C0    LDX   $C0EC     o_O
BD75-   10 FB       BPL   $BD72

; The nibble value is in the X register
; now. The lowest possible nibble value
; is $96 and the highest is $FF. To
; look up the offset in the table at
; $BB96, we index off $BB00 + X. Math!
BD77-   BD 00 BB    LDA   $BB00,X

; Now the accumulator has the offset
; into the table of individual 2-bit
; combinations ($BC00..$BCFF). Store
; that offset in a temporary buffer
; towards the end of the target page.
; (It will eventually get overwritten
; by full 8-bit bytes, but in the
; meantime it's a useful $56-byte
; scratch space.)
BD7A-   99 02 D1    STA   $D102,Y   o_O

; The EOR value is set at $BD6F
; each time through loop #1.
BD7D-   49 D1       EOR   #$D1      o_O

; The Y register started at #$AA
; (set by the "TAY" instruction
; at $BD39), so this loop reads
; a total of #$56 nibbles.
BD7F-   C8          INY
BD80-   D0 ED       BNE   $BD6F

Here endeth nibble loop #1.

Nibble loop #2 reads nibbles $56..$AB,
combines them with bits 0-1 of the
appropriate nibble from the first $56,
and stores them in bytes $00..$55 of
the target page in memory.

BD82-   A0 AA       LDY   #$AA
BD84-   AE EC C0    LDX   $C0EC     o_O
BD87-   10 FB       BPL   $BD84
BD89-   5D 00 BB    EOR   $BB00,X
BD8C-   BE 02 D1    LDX   $D102,Y   o_O
BD8F-   5D 02 BC    EOR   $BC02,X

; This address was set at $BD5A
; based on the target page (minus 1
; so we can add Y from #$AA..#$FF).
BD92-   99 56 D1    STA   $D156,Y   o_O
BD95-   C8          INY
BD96-   D0 EC       BNE   $BD84

Here endeth nibble loop #2.

Nibble loop #3 reads nibbles $AC..$101,
combines them with bits 2-3 of the
appropriate nibble from the first $56,
and stores them in bytes $56..$AB of
the target page in memory.

BD98-   29 FC       AND   #$FC
BD9A-   A0 AA       LDY   #$AA
BD9C-   AE EC C0    LDX   $C0EC     o_O
BD9F-   10 FB       BPL   $BD9C
BDA1-   5D 00 BB    EOR   $BB00,X
BDA4-   BE 02 D1    LDX   $D102,Y   o_O
BDA7-   5D 01 BC    EOR   $BC01,X

; This address was set at $BD5A
; based on the target page (minus 1
; so we can add Y from #$AA..#$FF).
BDAA-   99 AC D1    STA   $D1AC,Y   o_O
BDAD-   C8          INY
BDAE-   D0 EC       BNE   $BD9C

Here endeth nibble loop #3.

Loop #4 reads nibbles $102..$155,
combines them with bits 4-5 of the
appropriate nibble from the first $56,
and stores them in bytes $AC..$101 of
the target page in memory. (This
overwrites two bytes after the end of
the target page, but we'll restore
them later from the stack.)

BDB0-   29 FC       AND   #$FC
BDB2-   A2 AC       LDX   #$AC
BDB4-   AC EC C0    LDY   $C0EC     o_O
BDB7-   10 FB       BPL   $BDB4
BDB9-   59 00 BB    EOR   $BB00,Y
BDBC-   BC 00 D1    LDY   $D100,X   o_O
BDBF-   59 00 BC    EOR   $BC00,Y

; This address was set at $BD45
; based on the target page.
BDC2-   9D 00 D1    STA   $D100,X   o_O
BDC5-   E8          INX
BDC6-   D0 EC       BNE   $BDB4

Here endeth nibble loop #4.

; Finally, get the last nibble and
; convert it to a byte. This should
; equal all the previous bytes XOR'd
; together. (This is the standard
; checksum algorithm shared by all
; 16-sector disks.)
BDC8-   29 FC       AND   #$FC
BDCA-   AC EC C0    LDY   $C0EC     o_O
BDCD-   10 FB       BPL   $BDCA
BDCF-   59 00 BB    EOR   $BB00,Y

; set carry if value is anything
; but 0
BDD2-   C9 01       CMP   #$01

; Restore the original data in the
; two bytes after the target page.
; (This does not affect the carry
; flag, which we will check in a
; moment, but we need to restore
; these bytes now to balance out
; the pushing to the stack we did
; at $BD65.)
BDD4-   A0 01       LDY   #$01
BDD6-   68          PLA
BDD7-   99 00 D1    STA   $D100,Y   o_O
BDDA-   88          DEY
BDDB-   10 F9       BPL   $BDD6

; if data checksum failed at $BDD2,
; start over
BDDD-   B0 8A       BCS   $BD69

; This was set to the physical
; sector number (at $BD3F), so
; this is a index into the 16-
; byte array at $BFBC.
BDDF-   A0 D1       LDY   #$D1      o_O
BDE1-   8A          TXA

; store #$00 at this location in
; the sector array to indicate
; that we've read this sector
BDE2-   99 BC BF    STA   $BFBC,Y

; decrement sector count
BDE5-   CE EF BD    DEC   $BDEF
BDE8-   CE 22 BD    DEC   $BD22
BDEB-   38          SEC

; If the sectors-left-in-this-track
; count (in $BD22) isn't zero yet,
; loop back to read more sectors.
BDEC-   D0 EF       BNE   $BDDD

; If the total sector count (in $BDEF,
; set at $BD0A and decremented at
; $BDE5) is zero, then we're done
; reading this game block and we can
; return to the caller.
BDEE-   A2 D1       LDX   #$D1      o_O
BDF0-   F0 09       BEQ   $BDFB

; increment phase (twice, so it
; points to the next whole track)
BDF2-   EE 0C BF    INC   $BF0C
BDF5-   EE 0C BF    INC   $BF0C

; jump back to seek and read
; from the next track
BDF8-   4C 13 BD    JMP   $BD13

; Execution continues here (from
; $BDF0). We're all done, so turn off
; the drive motor and exit.
BDFB-   AD E8 C0    LDA   $C0E8     o_O
BDFE-   60          RTS

                   ~

              Chapter 19
 In Which We Return to the Boot Sector
  And Our Adventure Comes To A Swift
       But Satisfying Conclusion


Continuing from $08B8 in sector $00 of
Spareboot...

; set up a stack frame (more on this in
; a moment)
08B8-   A0 08       LDY   #$08
08BA-   B9 17 08    LDA   $0817,Y
08BD-   48          PHA
08BE-   88          DEY
08BF-   D0 F9       BNE   $08BA

; clear and display hi-res page 2
; (like the original game)
08C1-   A2 20       LDX   #$20
08C3-   98          TYA
08C4-   99 00 40    STA   $4000,Y
08C7-   C8          INY
08C8-   D0 FA       BNE   $08C4
08CA-   EE C6 08    INC   $08C6
08CD-   CA          DEX
08CE-   D0 F4       BNE   $08C4
08D0-   2C 50 C0    BIT   $C050
08D3-   2C 55 C0    BIT   $C055
08D6-   2C 52 C0    BIT   $C052
08D9-   2C 57 C0    BIT   $C057

; also clear that uninitialized address
; that the game uses to determine if it
; should reload the title screen code
; from disk
08DC-   8D 80 95    STA   $9580

; and exit via the stack we set up
08DF-   60          RTS

Given the novel stack use in the
original bootloader, it seemed only
fair to do the same thing in my crack.
Also, we're about to clobber this page
in memory as soon as we start loading
the game code, so the execution pointer
should probably be somewhere else
before that happens.

And where does this predefined stack
point? I'm glad you asked.

*818.

0818- F1 BF F4 BF F7 BF FF 1F

Remember stack addresses are off-by-1
by design, so this stack frame will
take us to $BFF2, $BFF5, $BFF8, and
$2000.

The first three addresses are inside
Spareboot, and they look like this:

*BFF2L

BFF2-   A9 14       LDA   #$14
BFF4-   2C A9 15    BIT   $15A9
BFF7-   2C A9 16    BIT   $16A9
BFFA-   4C 00 BD    JMP   $BD00

$BFFA is the vector to read a game
block. The other entry points before it
each set the accumulator to a different
ID value ($14, $15, or $16) then fall
through to the vector at $BFFA to read
a game block from disk.

What are game blocks $14, $15, and $16?
They didn't exist in the original disk,
which only went up to $13. But why not
reuse the same infrastructre to load
the initial game code into memory? Our
read_game_block routine is even more
flexible than the original disk; blocks
can be stored anywhere in memory and be
any multiple of $100 bytes.

Recall this portion of our master disk
map:

tr | sc | memory     | notes
---+----+------------+-----------------
01 | 10 | 0800..17FF | game code
02 | 0F | 1800..26FF |   (cont.)
03 | 10 | 6000..6FFF |   (cont.)
04 | 10 | 7000..7FFF |   (cont.)
05 | 0E | 8000..8DFF |   (cont.)

Well let's just say that game block $14
is a $1F00 byte block starting on track
$01 that should be loaded at $0800.

Game block $15 is a $2E00 byte block
starting on track $03 to be loaded at
$6000.

And game block $16 is the high scores,
a $100 byte block read from track $22
into $2600, which is where the game
loads it during boot before relocating.

Did you notice that $BFF5 and $BFF8 are
hidden in that listing of $BFF2? I took
a cue from The Badlands of the original
disk, but used for good.

*BFF2L

BFF2-   A9 14       LDA   #$14
BFF4-   2C A9 15    BIT   $15A9
BFF7-   2C A9 16    BIT   $16A9
BFFA-   4C 00 BD    JMP   $BD00

*BFF5L

BFF5-   A9 15       LDA   #$15
BFF7-   2C A9 16    BIT   $16A9
BFFA-   4C 00 BD    JMP   $BD00

*BFF8L

BFF8-   A9 16       LDA   #$16
BFFA-   4C 00 BD    JMP   $BD00

Each of these calls to read_game_block
will exit via RTS and "return" to the
next address we pushed to the stack,
until finally it "returns" to $2000 and
starts the game.

Quod erat liberandum.

                   ~

            Acknowledgments


Thanks to qkumba for writing Spareboot,
for reviewing drafts of this write-up,
and for cheering along the way. The
cheering was the best part.

---------------------------------------
A 4am & san inc crack          No. 3136
------------------EOF------------------
